diff --git a/app/package.json b/app/package.json index b47ee885..1e495436 100644 --- a/app/package.json +++ b/app/package.json @@ -36,9 +36,5 @@ "esbuild", "protobufjs", "sharp" - ], - "dependencies": { - "@types/luxon": "^3.6.2", - "luxon": "^3.6.1" - } + ] } diff --git a/app/packages/web/package.json b/app/packages/web/package.json index 73c4fa69..aa0b1dc0 100644 --- a/app/packages/web/package.json +++ b/app/packages/web/package.json @@ -14,10 +14,15 @@ "@astrojs/solid-js": "^5.1.0", "@astrojs/starlight": "^0.34.3", "@fontsource/ibm-plex-mono": "^5.2.5", + "@shikijs/transformers": "^3.4.2", + "@types/luxon": "^3.6.2", "ai": "^5.0.0-alpha.2", "astro": "^5.7.13", + "diff": "^8.0.2", + "luxon": "^3.6.1", "rehype-autolink-headings": "^7.1.0", "sharp": "^0.32.5", + "shiki": "^3.4.2", "solid-js": "^1.9.7", "toolbeam-docs-theme": "^0.2.4" } diff --git a/app/packages/web/src/components/CodeBlock.tsx b/app/packages/web/src/components/CodeBlock.tsx new file mode 100644 index 00000000..17559ece --- /dev/null +++ b/app/packages/web/src/components/CodeBlock.tsx @@ -0,0 +1,47 @@ +import { + type JSX, + onCleanup, + splitProps, + createEffect, + createResource, +} from "solid-js" +import { codeToHtml } from "shiki" +import { transformerNotationDiff } from '@shikijs/transformers' + +interface CodeBlockProps extends JSX.HTMLAttributes { + code: string + lang?: string +} +function CodeBlock(props: CodeBlockProps) { + const [local, rest] = splitProps(props, ["code", "lang"]) + let containerRef!: HTMLDivElement + + const [html] = createResource(async () => { + return (await codeToHtml(local.code, { + lang: local.lang || "text", + themes: { + light: 'github-light', + dark: 'github-dark', + }, + transformers: [ + transformerNotationDiff(), + ], + })) as string + }) + + onCleanup(() => { + if (containerRef) containerRef.innerHTML = "" + }) + + createEffect(() => { + if (html() && containerRef) { + containerRef.innerHTML = html() as string + } + }) + + return ( +
+ ) +} + +export default CodeBlock diff --git a/app/packages/web/src/components/DiffView.tsx b/app/packages/web/src/components/DiffView.tsx new file mode 100644 index 00000000..443fc6f4 --- /dev/null +++ b/app/packages/web/src/components/DiffView.tsx @@ -0,0 +1,66 @@ +import { type Component, createSignal, onMount } from "solid-js" +import { diffLines, type Change } from "diff" +import CodeBlock from "./CodeBlock" +import styles from "./diffView.module.css" + +type DiffRow = { + left: string + right: string + type: "added" | "removed" | "unchanged" +} + +interface DiffViewProps { + oldCode: string + newCode: string + lang?: string + class?: string +} + +const DiffView: Component = (props) => { + const [rows, setRows] = createSignal([]) + + onMount(() => { + const chunks = diffLines(props.oldCode, props.newCode) + const diffRows: DiffRow[] = [] + + chunks.forEach((chunk: Change) => { + const lines = chunk.value.split(/\r?\n/) + if (lines.at(-1) === "") lines.pop() + + lines.forEach((line) => { + diffRows.push({ + left: chunk.removed ? line : chunk.added ? "" : line, + right: chunk.added ? line : chunk.removed ? "" : line, + type: chunk.added ? "added" + : chunk.removed ? "removed" + : "unchanged", + }) + }) + }) + + setRows(diffRows) + }) + + return ( +
+ {rows().map((r) => ( +
+ + +
+ ))} +
+ ) +} + +export default DiffView diff --git a/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx index 12619b60..c9fbb068 100644 --- a/app/packages/web/src/components/Share.tsx +++ b/app/packages/web/src/components/Share.tsx @@ -6,6 +6,7 @@ import { Switch, onMount, onCleanup, + splitProps, createMemo, createEffect, createSignal, @@ -20,8 +21,13 @@ import { IconCpuChip, IconSparkles, IconUserCircle, + IconChevronDown, + IconChevronRight, + IconPencilSquare, IconWrenchScrewdriver, } from "./icons" +import CodeBlock from "./CodeBlock" +import DiffView from "./DiffView" import styles from "./share.module.css" import { type UIMessage } from "ai" import { createStore, reconcile } from "solid-js/store" @@ -59,6 +65,10 @@ type SessionInfo = { cost?: number } +function getFileType(path: string) { + return path.split('.').pop() +} + // Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]` function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { const entries: Array<[string, any]> = []; @@ -111,18 +121,48 @@ function ProviderIcon(props: { provider: string, size?: number }) { ) } +interface ResultsButtonProps extends JSX.HTMLAttributes { + results: boolean +} +function ResultsButton(props: ResultsButtonProps) { + const [local, rest] = splitProps(props, ["results"]) + return ( + + ) +} + interface TextPartProps extends JSX.HTMLAttributes { text: string expand?: boolean highlight?: boolean } -function TextPart({ text, expand, highlight, ...props }: TextPartProps) { +function TextPart(props: TextPartProps) { + const [local, rest] = splitProps(props, ["text", "expand", "highlight"]) const [expanded, setExpanded] = createSignal(false) const [overflowed, setOverflowed] = createSignal(false) let preEl: HTMLPreElement | undefined function checkOverflow() { - if (preEl && !expand) { + if (preEl && !local.expand) { setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1) } } @@ -133,7 +173,7 @@ function TextPart({ text, expand, highlight, ...props }: TextPartProps) { }) createEffect(() => { - text + local.text setTimeout(checkOverflow, 0) }) @@ -144,11 +184,11 @@ function TextPart({ text, expand, highlight, ...props }: TextPartProps) { return (
-
 (preEl = el)}>{text}
+
 (preEl = el)}>{local.text}
{overflowed() &&
- - {assistant().providerID} - - - {assistant().modelID} - +
+ + {assistant().providerID} + + + {assistant().modelID} + +
} @@ -517,19 +560,59 @@ export default function Share(props: { api: string }) {
- - System - - +
+ + System + + +
} + { /* Edit tool */} + + {part => { + const args = part().toolInvocation.args + const filePath = args.filePath + return ( + <> +
+
+ +
+
+
+
+
+ + Edit {filePath} + +
+ +
+
+ +
+ + ) + }} +
{ /* Tool call */}
- - {part().toolInvocation.toolName} - -
- - {([name, value]) => - <> -
-
{name}
-
{value}
- - } -
+
+ + {part().toolInvocation.toolName} + +
+ + {([name, value]) => + <> +
+
{name}
+
{value}
+ + } +
+
+ + +
+ showResults(e => !e)} + /> + + + +
+
+ + + +
- - - - - - - -
@@ -609,10 +702,12 @@ export default function Share(props: { api: string }) {
- - {part.type} - - +
+ + {part.type} + + +
diff --git a/app/packages/web/src/components/diffview.module.css b/app/packages/web/src/components/diffview.module.css new file mode 100644 index 00000000..94911d06 --- /dev/null +++ b/app/packages/web/src/components/diffview.module.css @@ -0,0 +1,70 @@ +.diff { + display: grid; + row-gap: 0; + border: 1px solid var(--sl-color-divider); + background-color: var(--sl-color-bg-surface); + border-radius: 0.25rem; + + [data-section="row"] { + display: grid; + grid-template-columns: 1fr 1fr; + + &:first-child [data-section="cell"] { + padding-top: 0.375rem; + } + &:last-child [data-section="cell"] { + padding-bottom: 0.375rem; + } + } + + [data-section="cell"] { + position: relative; + padding-left: 1.5ch; + padding: 0.25rem 0.5rem 0.25rem 1.5ch; + overflow-x: auto; + margin: 0; + + pre { + background-color: var(--sl-color-bg-surface) !important; + } + + &:first-child { + border-right: 1px solid var(--sl-color-divider); + } + } + + [data-diff-type="removed"] { + background-color: var(--sl-color-red-low); + + & > pre { + --shiki-dark-bg: var(--sl-color-red-low) !important; + background-color: transparent !important; + } + + &::before { + content: "-"; + position: absolute; + left: 0.5ch; + user-select: none; + color: var(--sl-color-red-high); + } + } + + [data-diff-type="added"] { + background-color: var(--sl-color-green-low); + + & > pre { + --shiki-dark-bg: var(--sl-color-green-low) !important; + background-color: transparent !important; + } + + &::before { + content: "+"; + position: absolute; + left: 0.6ch; + user-select: none; + color: var(--sl-color-green-high); + } + } +} + diff --git a/app/packages/web/src/components/share.module.css b/app/packages/web/src/components/share.module.css index 1c068162..e2ebccab 100644 --- a/app/packages/web/src/components/share.module.css +++ b/app/packages/web/src/components/share.module.css @@ -19,6 +19,33 @@ } } +[data-element-button-text] { + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + padding: 0; + color: var(--sl-color-text-secondary); + + &:hover { + color: var(--sl-color-text); + } + + &[data-element-button-more] { + display: flex; + align-items: center; + gap: 0.125rem; + + span[data-button-icon] { + line-height: 1; + opacity: 0.85; + svg { + display: block; + } + } + } +} + [data-element-label] { text-transform: uppercase; letter-spacing: 0.05em; @@ -154,7 +181,13 @@ padding: 0 0 0.375rem; display: flex; flex-direction: column; - gap: 0.5rem; + gap: 1rem; + + [data-part-tool-body] { + display: flex; + flex-direction: column; + gap: 0.375rem; + } span[data-part-title] { line-height: 18px; @@ -203,7 +236,17 @@ padding-left: 0.125rem; color: var(--sl-color-text-dimmed); } + } + [data-part-tool-result] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + + button { + font-size: 0.75rem; + } } } } @@ -274,3 +317,10 @@ } } } + +.code-block { + pre { + line-height: 1.4; + font-size: 0.75rem; + } +} diff --git a/app/packages/web/src/styles/custom.css b/app/packages/web/src/styles/custom.css index 450be431..9c4c71f0 100644 --- a/app/packages/web/src/styles/custom.css +++ b/app/packages/web/src/styles/custom.css @@ -2,3 +2,15 @@ --sl-color-bg-surface: var(--sl-color-bg-nav); --sl-color-divider: var(--sl-color-gray-5); } + +@media (prefers-color-scheme: dark) { + .shiki, + .shiki span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + /* Optional, if you also want font styles */ + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; + } +}