AI chat richtext - #442
Conversation
|
There was a problem hiding this comment.
Pull request overview
This PR upgrades Val Studio’s AI chat input from plain text to a rich-text editor (ProseMirror-backed) with inline images and “mention field” chips, and adds URL/session persistence so the active chat session can follow navigation between overlay and studio.
Changes:
- Add
AIChatEditor(schema, parsing/serialization, input rules, floating toolbar, node views) + tests for round-tripping and HTML serialization. - Update AI chat flow to accept rich content (
ChatDocument→ HTML-esque text), extract inline image keys into protocolimage_keyblocks, and render rich user bubbles. - Add session propagation: unborn/born session lifecycle in
useAI,?session=preservation inValRouter, and session mirroring viasessionStoragefor overlay↔studio continuity; add global chat actions context for “mention this field”.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui/spa/hooks/useAI.ts | Unborn/born session lifecycle; accept rich chat documents; inline image key extraction; session loading state. |
| packages/ui/spa/components/ValRouter.tsx | Preserve and manage ?session= across in-studio navigation; expose useSessionParam. |
| packages/ui/spa/components/ValProvider.tsx | Introduce AIChatActionsProvider so other UI parts can open chat / insert refs. |
| packages/ui/spa/components/ValOverlay.tsx | Persist overlay chat session via sessionStorage; append ?session= to studio links; wire shared chat editor ref. |
| packages/ui/spa/components/ToolsMenu.tsx | Wire useAI options to URL/sessionStorage; connect shared editor ref and loading indicator. |
| packages/ui/spa/components/Field.tsx | Add “Mention this field in AI chat” button using shared chat actions/editor ref. |
| packages/ui/spa/components/AIChatActionsContext.tsx | New context for opening chat and inserting field refs into the editor. |
| packages/ui/spa/components/AIChat.tsx | Replace textarea with rich editor; render rich user bubbles; session loading/thinking indicators. |
| packages/ui/spa/components/AIChat.stories.tsx | Update story callbacks for new onSendMessage(content) signature. |
| packages/ui/spa/components/AIChatEditor/** | New rich editor implementation (types, schema, serialize/parse, plugins, node views, tests). |
| packages/shared/src/internal/sessionStorage.ts | Add VAL_AI_SESSION_STORAGE_KEY. |
| examples/next/val.config.ts | Enable AI chat in the example config. |
| examples/next/app/blogs/[blog]/page.val.ts | Loosen title constraint; enable bold in richtext options in the example schema. |
Comments suppressed due to low confidence (1)
packages/ui/spa/hooks/useAI.ts:1305
URLSearchParamsalready handles percent-encoding. Pre-encoding values withencodeURIComponentcauses double-encoding (notablymimetypelikeimage/pngbecomesimage%252Fpng), which will break the upload endpoint’s parsing of query params.
queryParams.set("sessionid", encodeURIComponent(sid));
queryParams.set("width", encodeURIComponent(readRes.width || 0));
queryParams.set("height", encodeURIComponent(readRes.height || 0));
queryParams.set(
"mimetype",
| function markingInputRule(regex: RegExp, markType: MarkType): InputRule { | ||
| return new InputRule(regex, (state, match, start, end) => { | ||
| const captured = match[1]; | ||
| if (!captured) return null; | ||
| const tr = state.tr; | ||
| const startReplace = start; | ||
| const endReplace = end; | ||
| tr.replaceWith( | ||
| startReplace, | ||
| endReplace, | ||
| state.schema.text(captured, [markType.create()]), | ||
| ); | ||
| tr.removeStoredMark(markType); | ||
| return tr; | ||
| }); | ||
| } |
| export interface ChatListItemNode { | ||
| tag: "li"; | ||
| children: (ChatParagraphNode | ChatBulletListNode | ChatOrderedListNode)[]; | ||
| } |
| items.push({ | ||
| tag: "li", | ||
| children: children as ChatListItemNode["children"], | ||
| }); |
| removeBtn.addEventListener("mousedown", (e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| const pos = getPos(); | ||
| if (pos === undefined) return; | ||
| view.dispatch(view.state.tr.delete(pos, pos + node.nodeSize)); | ||
| view.focus(); | ||
| }); |
| .catch((err) => { | ||
| console.error("AI chat image upload failed", err); | ||
| view.state.doc.descendants((n, pos) => { | ||
| if (n.type === imageType && n.attrs.key === pendingKey) { | ||
| view.dispatch(view.state.tr.delete(pos, pos + n.nodeSize)); | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| }); |
| default: | ||
| return <>{children}</>; |
| const USER_HTML_TAG_RE = | ||
| /<\/?(?:p|h[1-3]|blockquote|ul|ol|li|strong|em|del|code|br|img|field)\b/i; |
No description provided.