feat: v1 — persistent shell, model gateway, artifact improvements (#1462)

This commit is contained in:
dancer 2026-03-20 09:37:02 +00:00 committed by GitHub
parent 3651670fb9
commit f9652b452a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
161 changed files with 5166 additions and 8009 deletions

View file

@ -8,7 +8,7 @@ import { renderToString } from "react-dom/server";
import { MessageResponse } from "@/components/ai-elements/message";
import { documentSchema } from "./config";
import { createSuggestionWidget, type UISuggestion } from "./suggestions";
import type { UISuggestion } from "./suggestions";
export const buildDocumentFromContent = (content: string) => {
const parser = DOMParser.fromSchema(documentSchema);
@ -26,7 +26,7 @@ export const buildContentFromDocument = (document: Node) => {
export const createDecorations = (
suggestions: UISuggestion[],
view: EditorView
_view: EditorView
) => {
const decorations: Decoration[] = [];
@ -37,6 +37,7 @@ export const createDecorations = (
suggestion.selectionEnd,
{
class: "suggestion-highlight",
"data-suggestion-id": suggestion.id,
},
{
suggestionId: suggestion.id,
@ -44,21 +45,7 @@ export const createDecorations = (
}
)
);
decorations.push(
Decoration.widget(
suggestion.selectionStart,
(currentView) => {
const { dom } = createSuggestionWidget(suggestion, currentView);
return dom;
},
{
suggestionId: suggestion.id,
type: "widget",
}
)
);
}
return DecorationSet.create(view.state.doc, decorations);
return DecorationSet.create(_view.state.doc, decorations);
};

View file

@ -1,13 +1,13 @@
import { createRoot } from "react-dom/client";
// biome-ignore lint/complexity/noStaticOnlyClass: "Needs to be static"
export class ReactRenderer {
static render(component: React.ReactElement, dom: HTMLElement) {
const root = createRoot(dom);
root.render(component);
export function renderReactComponent(
component: React.ReactElement,
dom: HTMLElement
) {
const root = createRoot(dom);
root.render(component);
return {
destroy: () => root.unmount(),
};
}
return {
destroy: () => root.unmount(),
};
}

View file

@ -1,13 +1,6 @@
import type { Node } from "prosemirror-model";
import { Plugin, PluginKey } from "prosemirror-state";
import {
type Decoration,
DecorationSet,
type EditorView,
} from "prosemirror-view";
import { createRoot } from "react-dom/client";
import type { ArtifactKind } from "@/components/artifact";
import { Suggestion as PreviewSuggestion } from "@/components/suggestion";
import { DecorationSet } from "prosemirror-view";
import type { Suggestion } from "@/lib/db/schema";
export interface UISuggestion extends Suggestion {
@ -66,71 +59,6 @@ export function projectWithPositions(
});
}
export function createSuggestionWidget(
suggestion: UISuggestion,
view: EditorView,
artifactKind: ArtifactKind = "text"
): { dom: HTMLElement; destroy: () => void } {
const dom = document.createElement("span");
const root = createRoot(dom);
dom.addEventListener("mousedown", (event) => {
event.preventDefault();
view.dom.blur();
});
const onApply = () => {
const { state, dispatch } = view;
const decorationTransaction = state.tr;
const currentState = suggestionsPluginKey.getState(state);
const currentDecorations = currentState?.decorations;
if (currentDecorations) {
const newDecorations = DecorationSet.create(
state.doc,
currentDecorations.find().filter((decoration: Decoration) => {
return decoration.spec.suggestionId !== suggestion.id;
})
);
decorationTransaction.setMeta(suggestionsPluginKey, {
decorations: newDecorations,
selected: null,
});
dispatch(decorationTransaction);
}
const textTransaction = view.state.tr.replaceWith(
suggestion.selectionStart,
suggestion.selectionEnd,
state.schema.text(suggestion.suggestedText)
);
textTransaction.setMeta("no-debounce", true);
dispatch(textTransaction);
};
root.render(
<PreviewSuggestion
artifactKind={artifactKind}
onApply={onApply}
suggestion={suggestion}
/>
);
return {
dom,
destroy: () => {
// Wrapping unmount in setTimeout to avoid synchronous unmounting during render
setTimeout(() => {
root.unmount();
}, 0);
},
};
}
export const suggestionsPluginKey = new PluginKey("suggestions");
export const suggestionsPlugin = new Plugin({
key: suggestionsPluginKey,
@ -154,5 +82,15 @@ export const suggestionsPlugin = new Plugin({
decorations(state) {
return this.getState(state)?.decorations ?? DecorationSet.empty;
},
handleDOMEvents: {
mousedown(_view, event) {
const target = event.target as HTMLElement;
if (target.closest(".suggestion-highlight")) {
event.preventDefault();
return true;
}
return false;
},
},
},
});