Repository navigation
feat(web): rebuild chat replies on shadcn with sources and citations - #231
Closed
a2751012962 wants to merge 18 commits into
Closed
a2751012962 wants to merge 18 commits into
a2751012962 wants to merge 18 commits into
Conversation
🚀 Deploying Preview to Cloudflare 🚀Preview Deployments by commit
|
a2751012962
force-pushed
the
feat/chat-answer-sources
branch
from
October 2, 2026 07:02
05556cd to
75a5a1e
Compare
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
The previous commit dropped vite-plugin-mcp and its import but kept the call, so every build failed with ViteMcp is not defined.
The chat UI can only cite what it knows the agent read. Sources whose URL the 4096-byte result limit cut off are dropped, since the model never saw them, and sources are stripped from the tool message so snippets are not sent to the model twice.
Signed-in chats store them in a new messages.sources column (scripts/migrations/add_message_sources.sql). Until that migration runs, Supabase rejects the column with PGRST204 and the message is saved again without its sources rather than lost.
Adds the primitives and the Sources, InlineCitation, and Suggestion elements the chat will use. Theme tokens map onto the existing slate and Illini palette. The default border colour applies only to data-slot elements and radius tokens keep Tailwind's defaults, so pages outside the chat render as before; dark: utilities need a .dark ancestor because the app is light-only.
The upstream trigger always shows the source's hostname; a long domain like housing.illinois.edu clutters Chinese prose, so the chat passes the source number instead.
Replaces the assistant-ui runtime and primitives. While a reply thinks, its header shows the current step and a timer; once the answer starts, a sources pill (AI Elements Sources) takes that place, links to those pages become numbered InlineCitation markers, and follow-up questions render as Suggestion chips. A sent question still scrolls to the top with the reply growing below it unfollowed; useTopAnchoredScroll takes over what turnAnchor="top" did. The regenerate button only renders once a handler is passed: the external-store runtime never had onReload, so it was always disabled. The inline web-search tool card is gone, since its query and count now live in the thinking steps and the sources pill.
Tailwind's preflight removes list-style and nothing restored it inside replies, so bulleted and numbered lists rendered as bare indented lines.
a2751012962
force-pushed
the
feat/chat-answer-sources
branch
from
October 2, 2026 07:14
75a5a1e to
65d6dc9
Compare
The shadcn carousel subscribes to reInit and select but only unsubscribed select, so every unmount leaked a reInit listener on the Embla instance.
React Doctor flagged its cyclomatic complexity (18). Each part now owns one branch; behaviour is unchanged.
One write keeps the events in order without awaiting each one in a loop. The tests move to the list API and now also cover ordering and an empty list.
The variant exports in badge and button and the carousel's setApi effect are shadcn and Embla API that other copied components depend on. The exemptions are per file and per rule, so new code in those files is still checked.
Collaborator
|
not yet at this stage |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
中文摘要
整理聊天回答的展示,同时把聊天界面从 assistant-ui 换到 shadcn/ui + AI Elements。基于 #220。
Suggestion),放不下时横向滑动。messages.sources,访客存 localStorage。需要执行迁移apps/web/scripts/migrations/add_message_sources.sql;没执行之前消息照常保存,只是不带来源。vendor-react少了 74 kB(gzip),聊天页 chunk 多了 11 kB,JS 合计少 63 kB。vite-plugin-mcp但还在调用ViteMcp());回答里的列表恢复显示圆点和序号。Why
A chat reply was one Markdown block. Sources only appeared as raw URLs the model wrote inline, follow-up questions were stacked vertically, and an inline "Web search" tool card repeated what the thinking steps already said. #220 moves the frontend to shadcn, so the new reply layout is built on shadcn/ui and AI Elements instead of on the assistant-ui runtime that is going away.
What changed
Worker.
web_searchreturns its results asmetadata.sources.buildObservationkeeps only the sources whose URL is still in the tool result after the registry's 4096-byte cap, since the model never read the ones that were cut off, and it stripssourcesfrom the tool message so the snippets are not sent to the model a second time. The loop emits onesource-urlSSE event per source right aftertool_result. The payload (sourceId,url,title,snippet) mirrors the AI SDKsource-urlpart, so it carries over if the chat later moves to the AI SDK stream protocol.Client.
deepseekSseparsessource-urlintoStreamChunk.source, accepting only http(s) URLs. A rejected payload never falls through to the legacy delta parser, so it can't leak into the answer text.useChatSessioncollects sources per reply, dedupes them by URL across searches, and saves them with the message.Persistence.
conversationServicewritesmessages.sources(jsonb). If PostgREST answersPGRST204because the column does not exist yet, the message is saved again without its sources.localConversationServicestores them for guests.UI (
src/components/chat/):ChatThreadowns the session and renders the list, empty state, and sticky composer.useTopAnchoredScrollreplaces assistant-ui'sturnAnchor="top": a spacer after the list (shrink-0, or flexbox squeezes it to nothing) makes the anchored position reachable, and loading a stored conversation jumps to the end instead.AssistantMessageshowsThinkingProcessuntil text arrives, thenMessageSources(AI ElementsSources).MarkdownContentrenders react-markdown directly and turns links to a source intoSourceCitation(AI ElementsInlineCitation). Matching tolerates the model's rewrites (http vs https,www., trailing slash,#fragment). A bare URL keeps only the number; a named link keeps its text before the number.ChatComposeris a plain form: Enter sends, Shift+Enter adds a line, and Enter that confirms an IME candidate is ignored (nativeEvent.isComposing).ChatRuntimeProvider, the assistant-ui message adapter and its tests, andWebSearchToolUI.@assistant-ui/reactand@assistant-ui/react-markdownare uninstalled.shadcn setup.
components.jsonsets thecnalias to@/utils/cn, following the repo'ssrc/utilsconvention, and registers@ai-elements.src/index.cssmaps the shadcn tokens onto the slate and Illini palette. To leave pages outside the chat untouched: the default border colour only applies to[data-slot]elements (in@layer base, so utilities still win), radius tokens keep Tailwind's defaults, anddark:needs a.darkancestor because the app is light-only. One local change to a copied component, marked in place:InlineCitationCardTriggertakes a custom label, because the upstream trigger always prints the hostname.Behavior changes
onReload, and assistant-ui disables Reload without it (reload: this._store.onReload !== void 0inexternal-store-thread-runtime-core.js).Before merging
Run
apps/web/scripts/migrations/add_message_sources.sqlon Supabase. Deploying without it is safe; replies are just saved without their sources until it runs.Verification
Baseline is #220's head plus the build fix, measured at
1d52b96before #220 was force-pushed. The rewrite changed only docs andpackage.jsonscripts, so the numbers carry over. "This branch" is the PR head, re-checked after rebasing onto the newfeat/frontend.tsc -p apps/web --noEmitnode --test "worker/**/*.test.ts" "src/**/*.test.ts"vp lint apps/web/src apps/web/workervp build(apps/web)vendor-reactJS (all pages)ChatPageJSindex.cssThe worker loop tests import
.txtprompts, and on Node 24node --testneeds a loader for them. That was already true before this PR.In the browser (
vp dev,/chat, with/api/chatreplaced by a scripted SSE stream in the real Worker format):1, bare URL →1only, trailing-slash variant →2,#fragmentvariant →3. A non-source link stayed an orange link. Citation numbers compute to slate-500, not the.prose aorange.Not verified here: a real Tavily search (no key locally), and the Supabase write path, including the
PGRST204fallback (no Supabase locally).CI: Workers Builds
Workers Builds: uiucfails on this PR, and it fails the same way onfeat/frontenditself. Going back along the base history, the last passing commit is71751f5and the first failing one is7654d56 chore(auth): init package(#217), whose only change is the newapps/authpackage and its lockfile update. Every commit after it fails, with or without this PR. Locally,pnpm install --frozen-lockfileandvp packinapps/authboth pass at that commit, so the real error is only in the Cloudflare build log, which the CLI credentials here can't read (403). This PR does fix the separateViteMcp is not definedbuild error that #220 introduced.