From 91e8deee74c627fee29c4c81833766040334170f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 30 Oct 2026 16:43:24 +0900 Subject: [PATCH] refac --- backend/open_webui/routers/comments.py | 7 +- src/lib/apis/comments/index.ts | 10 +- .../chat/Messages/ContentRenderer.svelte | 14 +- .../comments/CommentComposer.svelte | 46 +- .../comments/CommentHighlights.svelte | 488 ++++++++++++++++++ .../components/comments/CommentReplies.svelte | 4 +- .../components/comments/CommentsHost.svelte | 45 +- .../components/comments/CommentsPanel.svelte | 4 +- src/lib/components/comments/anchors.ts | 58 ++- src/lib/utils/index.ts | 8 + 10 files changed, 606 insertions(+), 78 deletions(-) create mode 100644 src/lib/components/comments/CommentHighlights.svelte diff --git a/backend/open_webui/routers/comments.py b/backend/open_webui/routers/comments.py index 0c480cd36d..869c0fd581 100644 --- a/backend/open_webui/routers/comments.py +++ b/backend/open_webui/routers/comments.py @@ -108,10 +108,12 @@ async def mentions( return result -async def list_comments(db, user, resource, resource_type, resource_id, parent_id, cursor, limit): +async def list_comments(db, user, resource, resource_type, resource_id, parent_id, cursor, limit, *, anchored=False): query = select(Comment).where( Comment.resource_type == resource_type, Comment.resource_id == resource_id, Comment.parent_id == parent_id ) + if anchored: + query = query.where(Comment.meta['anchor'].as_string().is_not(None), Comment.deleted_at.is_(None)) if cursor: try: timestamp, id = cursor.split(':', 1) @@ -138,11 +140,12 @@ async def list_resource_comments( resource_id: str, cursor: str | None = None, limit: int = Query(20, ge=1, le=50), + anchored: bool = False, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): resource = await comment_resource(resource_type, resource_id, user, db, request=request) - result = await list_comments(db, user, resource, resource_type, resource_id, None, cursor, limit) + result = await list_comments(db, user, resource, resource_type, resource_id, None, cursor, limit, anchored=anchored) result['total'] = await db.scalar( select(func.count()) .select_from(Comment) diff --git a/src/lib/apis/comments/index.ts b/src/lib/apis/comments/index.ts index fb0a2a5921..37ac440614 100644 --- a/src/lib/apis/comments/index.ts +++ b/src/lib/apis/comments/index.ts @@ -1,14 +1,7 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; export type CommentResource = { - resource_type: - | 'model' - | 'knowledge' - | 'prompt' - | 'skill' - | 'tool' - | 'note' - | 'chat'; + resource_type: 'model' | 'knowledge' | 'prompt' | 'skill' | 'tool' | 'note' | 'chat'; resource_id: string; }; export type CommentAnchor = { @@ -71,6 +64,7 @@ export async function commentsRequest( response.status, typeof data.detail === 'string' ? data.detail : 'Unable to load comments' ); + if (method !== 'GET') window.dispatchEvent(new Event('comments:updated')); return data; } export const resourceQuery = (resource: CommentResource) => diff --git a/src/lib/components/chat/Messages/ContentRenderer.svelte b/src/lib/components/chat/Messages/ContentRenderer.svelte index 4eb18b87d7..c0d05cf27c 100644 --- a/src/lib/components/chat/Messages/ContentRenderer.svelte +++ b/src/lib/components/chat/Messages/ContentRenderer.svelte @@ -2,10 +2,10 @@ import { commentSurface, contentHash, + commentSelectionDraft, selectedComment } from '$lib/components/comments/anchors'; import { - openComments, config, user, artifactCode, @@ -333,7 +333,8 @@
{ if (!commentSelection) return; - const { options, anchor } = commentSelection; - openComments( - { resource_type: options.resource_type, resource_id: options.resource_id }, - { target_type: options.target_type, target_id: options.target_id, anchor } - ); + commentSelectionDraft.set(commentSelection); window.getSelection()?.removeAllRanges(); closeFloatingButtons(); } diff --git a/src/lib/components/comments/CommentComposer.svelte b/src/lib/components/comments/CommentComposer.svelte index 3ec7612c6d..df18e0012d 100644 --- a/src/lib/components/comments/CommentComposer.svelte +++ b/src/lib/components/comments/CommentComposer.svelte @@ -10,6 +10,8 @@ export let draftKey: string; export let contextLabel = ''; export let quote = ''; + export let scrollable = true; + export let embedded = false; export let onClearContext: (() => void) | null = null; export let initialContent = ''; export let submitLabel = 'Comment'; @@ -88,35 +90,41 @@ : $settings?.chatDirection === 'LTR' ? 'ltr' : 'auto'} - class="relative rounded-2xl border border-gray-100/50 dark:border-gray-850/50 hover:border-gray-200 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition bg-white/5 dark:bg-gray-500/5 ps-3 pb-2.5 {contextLabel || - quote - ? 'pt-1' - : 'pt-2.5'} {($settings?.highContrastMode ?? false) + class="relative {embedded + ? '' + : `rounded-2xl border border-gray-100/50 dark:border-gray-850/50 hover:border-gray-200 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition bg-white/5 dark:bg-gray-500/5 ps-3 pb-2.5 ${contextLabel || quote ? 'pt-1' : 'pt-2.5'}`} {($settings?.highContrastMode ?? + false) ? 'focus-within:outline focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-blue-500' : ''}" > {#if contextLabel || quote} -
-
- {contextLabel}{#if onClearContext}{/if} -
+
+ {#if contextLabel || onClearContext} +
+ {contextLabel}{#if onClearContext}{/if} +
+ {/if} {#if quote}
{quote}
{/if}
{/if} -
+
{#if error}{/if} -
+
+
+
+ {draft.anchor.quote} +
+ {#key `${draft.options.resource_id}:${draft.options.target_id}:${draft.anchor.surface_id}:${draft.anchor.start}:${draft.anchor.end}`} + { + const created = await commentsRequest('/', 'POST', { + resource_type: draft.options.resource_type, + resource_id: draft.options.resource_id, + target_type: draft.options.target_type, + target_id: draft.options.target_id, + anchor: draft.anchor, + content, + temp_id + }); + if (!mounted || $commentSelectionDraft !== draft) return; + postedComment = created; + records = { + ...records, + [created.resource_id]: [ + ...(records[created.resource_id] ?? []).filter((item) => item.id !== created.id), + created + ] + }; + }} + /> + {/key} +
+ {/if} + {#each hovered as { comment } (comment.id)} +
+
+ + {comment.user?.name ?? $i18n.t('Deleted user')} + {dayjs(comment.created_at / 1e6).fromNow()} +
+ {#if comment.meta?.anchor} +
+ {comment.meta.anchor.quote} +
+ {/if} +
+ +
+ +
+ {/each} +
+
+ + diff --git a/src/lib/components/comments/CommentReplies.svelte b/src/lib/components/comments/CommentReplies.svelte index 8154be8a93..f8a399e846 100644 --- a/src/lib/components/comments/CommentReplies.svelte +++ b/src/lib/components/comments/CommentReplies.svelte @@ -3,7 +3,7 @@ import { commentsRequest, type ResourceComment } from '$lib/apis/comments'; import CommentItem from './CommentItem.svelte'; import CommentReplyComposer from './CommentReplyComposer.svelte'; - import { groupReplies } from '$lib/utils'; + import { groupReplies, scrollToComment } from '$lib/utils'; import Spinner from '$lib/components/common/Spinner.svelte'; export let parent: ResourceComment; export let replyTo: ResourceComment | null; @@ -71,7 +71,7 @@ if (linkedCommentId && revealedId !== linkedCommentId) { revealedId = linkedCommentId; await tick(); - document.getElementById(`comment-${linkedCommentId}`)?.scrollIntoView({ block: 'nearest' }); + if (alive && request === version) scrollToComment(linkedCommentId); } } catch (e) { if (alive && request === version) onError(e); diff --git a/src/lib/components/comments/CommentsHost.svelte b/src/lib/components/comments/CommentsHost.svelte index 0ba97c3a64..c7bc56af0a 100644 --- a/src/lib/components/comments/CommentsHost.svelte +++ b/src/lib/components/comments/CommentsHost.svelte @@ -17,7 +17,8 @@ import Modal from '$lib/components/common/Modal.svelte'; import Annotation from '$lib/components/icons/Annotation.svelte'; import CommentsPanel from './CommentsPanel.svelte'; - import { selectedComment } from './anchors'; + import CommentHighlights from './CommentHighlights.svelte'; + import { selectedComment, commentSelectionDraft } from './anchors'; import { toast } from 'svelte-sonner'; const i18n = getContext('i18n'); let selection: ReturnType = null; @@ -30,13 +31,19 @@ navigation += 1; deepLink = ''; commentsView.set(null); + commentSelectionDraft.set(null); selection = null; } - $: commentsAllowed = !!($config?.features?.enable_comments && $user && !$page.url.pathname.startsWith('/s/')); + $: commentsAllowed = !!( + $config?.features?.enable_comments && + $user && + !$page.url.pathname.startsWith('/s/') + ); let preparing = false; $: if (($user?.id ?? null) !== account) { account = $user?.id ?? null; commentsView.set(null); + commentSelectionDraft.set(null); selection = null; tick().then(() => commentDrafts.clear()); } @@ -59,17 +66,12 @@ toast.error(e instanceof Error ? e.message : String(e)); } }; - $: if ( - commentsAllowed && - $page.url.searchParams.get('comment') && - deepLink !== $page.url.href - ) { + $: if (commentsAllowed && $page.url.searchParams.get('comment') && deepLink !== $page.url.href) { deepLink = $page.url.href; loadLink($page.url.searchParams.get('comment')!); } const select = () => { - if (!preparing) - selection = commentsAllowed ? selectedComment() : null; + if (!preparing) selection = commentsAllowed ? selectedComment() : null; }; const commentSelection = async () => { const selected = selection; @@ -80,17 +82,19 @@ try { await selected.options.prepare?.(); if (version !== navigation || accountId !== account) return; - openComments( - { - resource_type: selected.options.resource_type, - resource_id: selected.options.resource_id - }, - { - target_type: selected.options.target_type, - target_id: selected.options.target_id, - anchor: selected.anchor - } - ); + if (selected.options.resource_type === 'chat') commentSelectionDraft.set(selected); + else + openComments( + { + resource_type: selected.options.resource_type, + resource_id: selected.options.resource_id + }, + { + target_type: selected.options.target_type, + target_id: selected.options.target_id, + anchor: selected.anchor + } + ); window.getSelection()?.removeAllRanges(); selection = null; } catch (e) { @@ -119,6 +123,7 @@ on:scroll|capture={() => (selection = null)} /> {#if commentsAllowed} + {#key account}{/key} {#if selection && !selection.options.toolbar_id}