feat: Geteilte Links auf Dashboard anzeigen + URL-Fix
Dashboard: - Neue Section 'Geteilte Links' zeigt alle aktiven Shares - Pro Eintrag: Role-Icon, Text-Vorschau, Session-Name, Modus, Datum - Buttons: Link kopieren, öffnen, löschen (mit sofortigem UI-Update) - Section nur sichtbar wenn Shares vorhanden Backend: - SharedEndpoint HandleListShares erweitert: Nachricht-Preview, Role, Session-Name pro Share mitgeliefert Frontend: - apiListShares() Funktion hinzugefügt - Bugfix: Share-URL '/#/shared/' → '/#shared/' (Extra-Slash verursachte 404 weil parseHash leeren Page-Namen ergab)
This commit is contained in:
1 parent
4bdcebc1e1
commit
38a8223228
5 files changed
+148
-1
No files matched your search
@@ -161,7 +161,38 @@ public static class SharedEndpoint
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
var shares = await db.GetSharedMessagesByUser(userId);
|
||||
await ctx.Response.WriteAsJsonAsync(shares);
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var s in shares)
|
||||
{
|
||||
var message = await db.GetMessage(s.MessageId);
|
||||
string sessionName = null;
|
||||
if (message != null)
|
||||
{
|
||||
var chain = await db.GetChain(message.ChainId);
|
||||
if (chain != null)
|
||||
{
|
||||
var session = await db.GetSession(chain.SessionId);
|
||||
sessionName = session?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
var preview = message?.Content ?? "";
|
||||
if (preview.Length > 150) preview = preview.Substring(0, 150) + "…";
|
||||
|
||||
result.Add(new
|
||||
{
|
||||
s.ShareToken,
|
||||
s.MessageId,
|
||||
s.IncludeContext,
|
||||
s.CreatedAt,
|
||||
role = message?.Role,
|
||||
preview,
|
||||
sessionName
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(result);
|
||||
}
|
||||
|
||||
private static async Task HandleDeleteShare(HttpContext ctx, DatabaseService db)
|
||||
|
||||
@@ -256,6 +256,66 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Shared Links */
|
||||
|
||||
.shared-links-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--badge-spacing);
|
||||
}
|
||||
|
||||
.shared-link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--box-spacing);
|
||||
padding: var(--box-padding);
|
||||
background: var(--f3-bg);
|
||||
border: var(--border-width) solid var(--f3-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.shared-link-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--badge-spacing) / 2);
|
||||
}
|
||||
|
||||
.shared-link-preview {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.shared-link-preview .fa {
|
||||
opacity: var(--semi-transparent);
|
||||
margin-right: calc(var(--badge-spacing) / 2);
|
||||
}
|
||||
|
||||
.shared-link-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--box-spacing);
|
||||
opacity: var(--semi-transparent);
|
||||
font-size: calc(var(--font-size) * 0.9);
|
||||
}
|
||||
|
||||
.shared-link-actions {
|
||||
display: flex;
|
||||
gap: var(--badge-spacing);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.danger-icon {
|
||||
color: var(--dangerous);
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
.danger-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Compact cards (no image) */
|
||||
.card-compact {
|
||||
padding: var(--box-padding);
|
||||
|
||||
@@ -80,6 +80,12 @@
|
||||
</div>
|
||||
<div class="card-grid" id="agentCards"></div>
|
||||
</section>
|
||||
<section id="sharedLinksSection" hidden>
|
||||
<div class="section-header">
|
||||
<h2>Geteilte Links</h2>
|
||||
</div>
|
||||
<div class="shared-links-list" id="sharedLinksList"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -376,6 +376,11 @@ async function apiRunCronjob(id) {
|
||||
|
||||
// ── Shared Message API ──
|
||||
|
||||
async function apiListShares() {
|
||||
const resp = await fetch('/api/shared');
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiShareMessage(messageId, includeContext = false) {
|
||||
const resp = await fetch(`/api/messages/${messageId}/share`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -325,6 +325,51 @@ registerPage('dashboard', {
|
||||
|
||||
// Neuer Cronjob Button
|
||||
document.getElementById('newCronjobBtn').addEventListener('click', () => navigateTo('cronjob'));
|
||||
|
||||
// Shared Links
|
||||
const shares = await apiListShares();
|
||||
const sharedSection = document.getElementById('sharedLinksSection');
|
||||
const sharedList = document.getElementById('sharedLinksList');
|
||||
if (shares.length > 0) {
|
||||
sharedSection.hidden = false;
|
||||
sharedList.innerHTML = '';
|
||||
for (const s of shares) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'shared-link-row';
|
||||
const shareUrl = `${location.origin}/#shared/${s.shareToken}`;
|
||||
const modeLabel = s.includeContext ? 'Thread' : 'Einzeln';
|
||||
const roleIcon = s.role === 'user' ? 'fa-user' : 'fa-robot';
|
||||
row.innerHTML = `
|
||||
<div class="shared-link-info">
|
||||
<span class="shared-link-preview"><i class="fa ${roleIcon}"></i> ${esc(s.preview || '(leer)')}</span>
|
||||
<span class="shared-link-meta">
|
||||
${s.sessionName ? `<span><i class="fa fa-comment"></i> ${esc(s.sessionName)}</span>` : ''}
|
||||
<span><i class="fa fa-${s.includeContext ? 'comments' : 'comment-o'}"></i> ${modeLabel}</span>
|
||||
<span><i class="fa fa-clock-o"></i> ${formatDate(s.createdAt)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="shared-link-actions">
|
||||
<button class="icon-btn" data-action="copy" title="Link kopieren"><i class="fa fa-clipboard"></i></button>
|
||||
<button class="icon-btn" data-action="open" title="Öffnen"><i class="fa fa-external-link"></i></button>
|
||||
<button class="icon-btn danger-icon" data-action="delete" title="Link löschen"><i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
row.querySelector('[data-action="copy"]').addEventListener('click', async () => {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
showToast('Link kopiert!');
|
||||
});
|
||||
row.querySelector('[data-action="open"]').addEventListener('click', () => {
|
||||
window.open(shareUrl, '_blank');
|
||||
});
|
||||
row.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||
await apiDeleteShare(s.shareToken);
|
||||
row.remove();
|
||||
if (sharedList.children.length === 0) sharedSection.hidden = true;
|
||||
showToast('Link gelöscht');
|
||||
});
|
||||
sharedList.appendChild(row);
|
||||
}
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
if (this._closeCardMenus) {
|
||||
|
||||
Reference in new issue
Block a user