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 userId = ctx.GetUserId();
|
||||||
var shares = await db.GetSharedMessagesByUser(userId);
|
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)
|
private static async Task HandleDeleteShare(HttpContext ctx, DatabaseService db)
|
||||||
|
|||||||
@@ -256,6 +256,66 @@
|
|||||||
color: var(--text);
|
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) */
|
/* Compact cards (no image) */
|
||||||
.card-compact {
|
.card-compact {
|
||||||
padding: var(--box-padding);
|
padding: var(--box-padding);
|
||||||
|
|||||||
@@ -80,6 +80,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-grid" id="agentCards"></div>
|
<div class="card-grid" id="agentCards"></div>
|
||||||
</section>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -376,6 +376,11 @@ async function apiRunCronjob(id) {
|
|||||||
|
|
||||||
// ── Shared Message API ──
|
// ── Shared Message API ──
|
||||||
|
|
||||||
|
async function apiListShares() {
|
||||||
|
const resp = await fetch('/api/shared');
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
async function apiShareMessage(messageId, includeContext = false) {
|
async function apiShareMessage(messageId, includeContext = false) {
|
||||||
const resp = await fetch(`/api/messages/${messageId}/share`, {
|
const resp = await fetch(`/api/messages/${messageId}/share`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -325,6 +325,51 @@ registerPage('dashboard', {
|
|||||||
|
|
||||||
// Neuer Cronjob Button
|
// Neuer Cronjob Button
|
||||||
document.getElementById('newCronjobBtn').addEventListener('click', () => navigateTo('cronjob'));
|
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() {
|
destroy() {
|
||||||
if (this._closeCardMenus) {
|
if (this._closeCardMenus) {
|
||||||
|
|||||||
Reference in new issue
Block a user