mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 02:04:22 +01:00
wip: desktop work
This commit is contained in:
@@ -17,8 +17,15 @@ import type { WriteTool } from "opencode/tool/write"
|
|||||||
import type { TodoWriteTool } from "opencode/tool/todo"
|
import type { TodoWriteTool } from "opencode/tool/todo"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
|
|
||||||
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
|
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; lastToolOnly?: boolean }) {
|
||||||
const filteredParts = createMemo(() => props.parts?.filter((x) => x.type !== "tool" || x.tool !== "todoread"))
|
const filteredParts = createMemo(() => {
|
||||||
|
let tool = false
|
||||||
|
return props.parts?.filter((x) => {
|
||||||
|
if (x.type === "tool" && props.lastToolOnly && tool) return false
|
||||||
|
if (x.type === "tool") tool = true
|
||||||
|
return x.type !== "tool" || x.tool !== "todoread"
|
||||||
|
})
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div class="w-full flex flex-col items-start gap-4">
|
<div class="w-full flex flex-col items-start gap-4">
|
||||||
<For each={filteredParts()}>
|
<For each={filteredParts()}>
|
||||||
@@ -71,17 +78,14 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
|
|||||||
// const permission = permissions[permissionIndex]
|
// const permission = permissions[permissionIndex]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dynamic
|
||||||
<Dynamic
|
component={render}
|
||||||
component={render}
|
input={input}
|
||||||
input={input}
|
tool={props.part.tool}
|
||||||
tool={props.part.tool}
|
metadata={metadata}
|
||||||
metadata={metadata}
|
// permission={permission?.metadata ?? {}}
|
||||||
// permission={permission?.metadata ?? {}}
|
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
|
||||||
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
|
/>
|
||||||
/>
|
|
||||||
{/* <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show> */}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -166,6 +170,9 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
|
|||||||
<Collapsible.Content>{props.children}</Collapsible.Content>
|
<Collapsible.Content>{props.children}</Collapsible.Content>
|
||||||
</Show>
|
</Show>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
// <>
|
||||||
|
// <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show>
|
||||||
|
// </>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import { DateTime } from "luxon"
|
|||||||
|
|
||||||
interface PartBase {
|
interface PartBase {
|
||||||
content: string
|
content: string
|
||||||
|
start: number
|
||||||
|
end: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextPart extends PartBase {
|
export interface TextPart extends PartBase {
|
||||||
type: "text"
|
type: "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileAttachmentPart extends PartBase {
|
export interface FileAttachmentPart extends PartBase {
|
||||||
type: "file"
|
type: "file"
|
||||||
path: string
|
path: string
|
||||||
selection?: TextSelection
|
selection?: TextSelection
|
||||||
@@ -34,7 +36,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
let editorRef!: HTMLDivElement
|
let editorRef!: HTMLDivElement
|
||||||
|
|
||||||
const defaultParts = [{ type: "text", content: "" } as const]
|
const defaultParts = [{ type: "text", content: "", start: 0, end: 0 } as const]
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
contentParts: ContentPart[]
|
contentParts: ContentPart[]
|
||||||
popoverIsOpen: boolean
|
popoverIsOpen: boolean
|
||||||
@@ -51,7 +53,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const plainText = (event.clipboardData || window.clipboardData)?.getData("text/plain") ?? ""
|
const plainText = (event.clipboardData || window.clipboardData)?.getData("text/plain") ?? ""
|
||||||
addPart({ type: "text", content: plainText })
|
addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -74,7 +76,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
key: (x) => x,
|
key: (x) => x,
|
||||||
onSelect: (path) => {
|
onSelect: (path) => {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
addPart({ type: "file", path, content: "@" + getFilename(path) })
|
addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 })
|
||||||
setStore("popoverIsOpen", false)
|
setStore("popoverIsOpen", false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -117,17 +119,26 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const parseFromDOM = (): ContentPart[] => {
|
const parseFromDOM = (): ContentPart[] => {
|
||||||
const newParts: ContentPart[] = []
|
const newParts: ContentPart[] = []
|
||||||
|
let position = 0
|
||||||
editorRef.childNodes.forEach((node) => {
|
editorRef.childNodes.forEach((node) => {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
if (node.textContent) newParts.push({ type: "text", content: node.textContent })
|
if (node.textContent) {
|
||||||
|
const content = node.textContent
|
||||||
|
newParts.push({ type: "text", content, start: position, end: position + content.length })
|
||||||
|
position += content.length
|
||||||
|
}
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type) {
|
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type) {
|
||||||
switch ((node as HTMLElement).dataset.type) {
|
switch ((node as HTMLElement).dataset.type) {
|
||||||
case "file":
|
case "file":
|
||||||
|
const content = node.textContent!
|
||||||
newParts.push({
|
newParts.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
path: (node as HTMLElement).dataset.path!,
|
path: (node as HTMLElement).dataset.path!,
|
||||||
content: node.textContent!,
|
content,
|
||||||
|
start: position,
|
||||||
|
end: position + content.length,
|
||||||
})
|
})
|
||||||
|
position += content.length
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -163,17 +174,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const startIndex = atMatch ? atMatch.index! : cursorPosition
|
const startIndex = atMatch ? atMatch.index! : cursorPosition
|
||||||
const endIndex = atMatch ? cursorPosition : cursorPosition
|
const endIndex = atMatch ? cursorPosition : cursorPosition
|
||||||
|
|
||||||
const pushText = (acc: { parts: ContentPart[] }, value: string) => {
|
const pushText = (acc: { parts: ContentPart[]; runningIndex: number }, value: string) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
const last = acc.parts[acc.parts.length - 1]
|
const last = acc.parts[acc.parts.length - 1]
|
||||||
if (last && last.type === "text") {
|
if (last && last.type === "text") {
|
||||||
acc.parts[acc.parts.length - 1] = {
|
acc.parts[acc.parts.length - 1] = {
|
||||||
type: "text",
|
type: "text",
|
||||||
content: last.content + value,
|
content: last.content + value,
|
||||||
|
start: last.start,
|
||||||
|
end: last.end + value.length,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
acc.parts.push({ type: "text", content: value })
|
acc.parts.push({ type: "text", content: value, start: acc.runningIndex, end: acc.runningIndex + value.length })
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -183,20 +196,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
} = store.contentParts.reduce(
|
} = store.contentParts.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
if (acc.inserted) {
|
if (acc.inserted) {
|
||||||
acc.parts.push(item)
|
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
|
||||||
acc.runningIndex += item.content.length
|
acc.runningIndex += item.content.length
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextIndex = acc.runningIndex + item.content.length
|
const nextIndex = acc.runningIndex + item.content.length
|
||||||
if (nextIndex <= startIndex) {
|
if (nextIndex <= startIndex) {
|
||||||
acc.parts.push(item)
|
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
|
||||||
acc.runningIndex = nextIndex
|
acc.runningIndex = nextIndex
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type !== "text") {
|
if (item.type !== "text") {
|
||||||
acc.parts.push(item)
|
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
|
||||||
acc.runningIndex = nextIndex
|
acc.runningIndex = nextIndex
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
@@ -207,24 +220,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const tail = item.content.slice(tailLength)
|
const tail = item.content.slice(tailLength)
|
||||||
|
|
||||||
pushText(acc, head)
|
pushText(acc, head)
|
||||||
|
acc.runningIndex += head.length
|
||||||
|
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
pushText(acc, part.content)
|
pushText(acc, part.content)
|
||||||
|
acc.runningIndex += part.content.length
|
||||||
}
|
}
|
||||||
if (part.type !== "text") {
|
if (part.type !== "text") {
|
||||||
acc.parts.push({ ...part })
|
acc.parts.push({ ...part, start: acc.runningIndex, end: acc.runningIndex + part.content.length })
|
||||||
|
acc.runningIndex += part.content.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsGap = Boolean(atMatch)
|
const needsGap = Boolean(atMatch)
|
||||||
const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail
|
const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail
|
||||||
pushText(acc, rest)
|
pushText(acc, rest)
|
||||||
|
acc.runningIndex += rest.length
|
||||||
|
|
||||||
const baseCursor = startIndex + part.content.length
|
const baseCursor = startIndex + part.content.length
|
||||||
const cursorAddition = needsGap && rest.length > 0 ? 1 : 0
|
const cursorAddition = needsGap && rest.length > 0 ? 1 : 0
|
||||||
acc.cursorPositionAfter = baseCursor + cursorAddition
|
acc.cursorPositionAfter = baseCursor + cursorAddition
|
||||||
|
|
||||||
acc.inserted = true
|
acc.inserted = true
|
||||||
acc.runningIndex = nextIndex
|
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -237,9 +253,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
if (!inserted) {
|
if (!inserted) {
|
||||||
const baseParts = store.contentParts.filter((item) => !(item.type === "text" && item.content === ""))
|
const baseParts = store.contentParts.filter((item) => !(item.type === "text" && item.content === ""))
|
||||||
const appendedAcc = { parts: [...baseParts] as ContentPart[] }
|
const runningIndex = baseParts.reduce((sum, p) => sum + p.content.length, 0)
|
||||||
if (part.type === "text") pushText(appendedAcc, part.content)
|
const appendedAcc = { parts: [...baseParts] as ContentPart[], runningIndex }
|
||||||
if (part.type !== "text") appendedAcc.parts.push({ ...part })
|
if (part.type === "text") {
|
||||||
|
pushText(appendedAcc, part.content)
|
||||||
|
}
|
||||||
|
if (part.type !== "text") {
|
||||||
|
appendedAcc.parts.push({
|
||||||
|
...part,
|
||||||
|
start: appendedAcc.runningIndex,
|
||||||
|
end: appendedAcc.runningIndex + part.content.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : defaultParts
|
const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : defaultParts
|
||||||
setStore("contentParts", next)
|
setStore("contentParts", next)
|
||||||
setStore("popoverIsOpen", false)
|
setStore("popoverIsOpen", false)
|
||||||
|
|||||||
@@ -429,13 +429,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
.sort((a, b) => b.id.localeCompare(a.id)),
|
.sort((a, b) => b.id.localeCompare(a.id)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const working = createMemo(() => {
|
|
||||||
const last = messages()[messages().length - 1]
|
|
||||||
if (!last) return false
|
|
||||||
if (last.role === "user") return true
|
|
||||||
return !last.time.completed
|
|
||||||
})
|
|
||||||
|
|
||||||
const cost = createMemo(() => {
|
const cost = createMemo(() => {
|
||||||
const total = pipe(
|
const total = pipe(
|
||||||
messages(),
|
messages(),
|
||||||
@@ -487,6 +480,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const getMessageText = (message: Message | Message[] | undefined): string => {
|
const getMessageText = (message: Message | Message[] | undefined): string => {
|
||||||
if (!message) return ""
|
if (!message) return ""
|
||||||
if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
|
if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
|
||||||
|
const fileParts = sync.data.part[message.id]?.filter((p) => p.type === "file")
|
||||||
|
console.log(fileParts)
|
||||||
|
|
||||||
return sync.data.part[message.id]
|
return sync.data.part[message.id]
|
||||||
?.filter((p) => p.type === "text")
|
?.filter((p) => p.type === "text")
|
||||||
?.filter((p) => !p.synthetic)
|
?.filter((p) => !p.synthetic)
|
||||||
@@ -506,7 +502,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
messages,
|
messages,
|
||||||
messagesWithValidParts,
|
messagesWithValidParts,
|
||||||
userMessages,
|
userMessages,
|
||||||
working,
|
// working,
|
||||||
getMessageText,
|
getMessageText,
|
||||||
setActive(sessionId: string | undefined) {
|
setActive(sessionId: string | undefined) {
|
||||||
setStore("active", sessionId)
|
setStore("active", sessionId)
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import { Button, List, SelectDialog, Tooltip, IconButton, Tabs, Icon, Accordion, Diff } from "@opencode-ai/ui"
|
import {
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
SelectDialog,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
Tabs,
|
||||||
|
Icon,
|
||||||
|
Accordion,
|
||||||
|
Diff,
|
||||||
|
Collapsible,
|
||||||
|
} from "@opencode-ai/ui"
|
||||||
import { FileIcon } from "@/ui"
|
import { FileIcon } from "@/ui"
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js"
|
import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js"
|
||||||
import { useLocal, type LocalFile, type TextSelection } from "@/context/local"
|
import { useLocal, type LocalFile } from "@/context/local"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
import { getDirectory, getFilename } from "@/utils"
|
||||||
import { ContentPart, PromptInput } from "@/components/prompt-input"
|
import { ContentPart, PromptInput } from "@/components/prompt-input"
|
||||||
@@ -185,42 +196,10 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
interface SubmissionAttachment {
|
|
||||||
path: string
|
|
||||||
selection?: TextSelection
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const createAttachmentKey = (path: string, selection?: TextSelection) => {
|
|
||||||
if (!selection) return path
|
|
||||||
return `${path}:${selection.startLine}:${selection.startChar}:${selection.endLine}:${selection.endChar}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatAttachmentLabel = (path: string, selection?: TextSelection) => {
|
|
||||||
if (!selection) return getFilename(path)
|
|
||||||
return `${getFilename(path)} (${selection.startLine}-${selection.endLine})`
|
|
||||||
}
|
|
||||||
|
|
||||||
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
|
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
|
||||||
|
|
||||||
const text = parts.map((part) => part.content).join("")
|
const text = parts.map((part) => part.content).join("")
|
||||||
const attachments = new Map<string, SubmissionAttachment>()
|
const attachments = parts.filter((part) => part.type === "file")
|
||||||
|
|
||||||
const registerAttachment = (path: string, selection: TextSelection | undefined, label?: string) => {
|
|
||||||
if (!path) return
|
|
||||||
const key = createAttachmentKey(path, selection)
|
|
||||||
if (attachments.has(key)) return
|
|
||||||
attachments.set(key, {
|
|
||||||
path,
|
|
||||||
selection,
|
|
||||||
label: label ?? formatAttachmentLabel(path, selection),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptAttachments = parts.filter((part) => part.type === "file")
|
|
||||||
for (const part of promptAttachments) {
|
|
||||||
registerAttachment(part.path, part.selection, part.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// const activeFile = local.context.active()
|
// const activeFile = local.context.active()
|
||||||
// if (activeFile) {
|
// if (activeFile) {
|
||||||
@@ -239,7 +218,7 @@ export default function Page() {
|
|||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const attachmentParts = Array.from(attachments.values()).map((attachment) => {
|
const attachmentParts = attachments.map((attachment) => {
|
||||||
const absolute = toAbsolutePath(attachment.path)
|
const absolute = toAbsolutePath(attachment.path)
|
||||||
const query = attachment.selection
|
const query = attachment.selection
|
||||||
? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}`
|
? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}`
|
||||||
@@ -252,9 +231,9 @@ export default function Page() {
|
|||||||
source: {
|
source: {
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
text: {
|
text: {
|
||||||
value: `@${attachment.label}`,
|
value: attachment.content,
|
||||||
start: 0,
|
start: attachment.start,
|
||||||
end: 0,
|
end: attachment.end,
|
||||||
},
|
},
|
||||||
path: absolute,
|
path: absolute,
|
||||||
},
|
},
|
||||||
@@ -510,8 +489,8 @@ export default function Page() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs.Content value="chat" class="select-text flex flex-col flex-1 min-h-0">
|
<Tabs.Content value="chat" class="select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
|
||||||
<div class="p-6 pt-12 max-w-[904px] w-full mx-auto flex flex-col flex-1 min-h-0">
|
<div class="px-6 pt-12 max-w-[904px] w-full mx-auto flex flex-col flex-1 min-h-0">
|
||||||
<Show
|
<Show
|
||||||
when={local.session.active()}
|
when={local.session.active()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -537,7 +516,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(activeSession) => (
|
{(activeSession) => (
|
||||||
<div class="py-3 flex flex-col flex-1 min-h-0">
|
<div class="pt-3 flex flex-col flex-1 min-h-0">
|
||||||
<div class="flex items-start gap-8 flex-1 min-h-0">
|
<div class="flex items-start gap-8 flex-1 min-h-0">
|
||||||
<Show when={local.session.userMessages().length > 1}>
|
<Show when={local.session.userMessages().length > 1}>
|
||||||
<ul role="list" class="w-60 shrink-0 flex flex-col items-start gap-1">
|
<ul role="list" class="w-60 shrink-0 flex flex-col items-start gap-1">
|
||||||
@@ -665,30 +644,65 @@ export default function Page() {
|
|||||||
(m) => m.role === "assistant" && m.parentID == message.id,
|
(m) => m.role === "assistant" && m.parentID == message.id,
|
||||||
) as AssistantMessageType[]
|
) as AssistantMessageType[]
|
||||||
})
|
})
|
||||||
|
const working = createMemo(() => {
|
||||||
|
const last = assistantMessages()[assistantMessages().length - 1]
|
||||||
|
if (!last) return false
|
||||||
|
return !last.time.completed
|
||||||
|
})
|
||||||
|
const lastWithContent = createMemo(() =>
|
||||||
|
assistantMessages().findLast((m) => {
|
||||||
|
const parts = sync.data.part[m.id]
|
||||||
|
return parts?.find((p) => p.type === "text" || p.type === "tool")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-message={message.id} class="flex flex-col items-start self-stretch gap-8">
|
||||||
data-message={message.id}
|
|
||||||
class="flex flex-col items-start self-stretch gap-14 pt-1.5"
|
|
||||||
>
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div class="flex flex-col items-start gap-2 self-stretch">
|
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
|
||||||
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
||||||
{title() ?? prompt()}
|
{title() ?? prompt()}
|
||||||
</h1>
|
</h1>
|
||||||
<Show when={title}>
|
</div>
|
||||||
<div class="text-12-regular text-text-base">{prompt()}</div>
|
<Show when={title}>
|
||||||
|
<div class="-mt-5 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
|
||||||
|
</Show>
|
||||||
|
{/* Response */}
|
||||||
|
<div class="w-full flex flex-col gap-2">
|
||||||
|
<Collapsible variant="ghost">
|
||||||
|
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
|
||||||
|
<div class="flex items-center gap-1 self-stretch">
|
||||||
|
<h2 class="text-12-medium">Show steps</h2>
|
||||||
|
<Collapsible.Arrow />
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div class="w-full flex flex-col items-start self-stretch gap-8">
|
||||||
|
<For each={assistantMessages()}>
|
||||||
|
{(assistantMessage) => {
|
||||||
|
const parts = createMemo(() => sync.data.part[assistantMessage.id])
|
||||||
|
return <AssistantMessage message={assistantMessage} parts={parts()} />
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible>
|
||||||
|
<Show when={working() && lastWithContent()}>
|
||||||
|
{(last) => {
|
||||||
|
const lastParts = createMemo(() => sync.data.part[last().id])
|
||||||
|
return (
|
||||||
|
<AssistantMessage lastToolOnly message={last()} parts={lastParts()} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<div class="w-full flex flex-col gap-6 items-start self-stretch">
|
<Show when={!working()}>
|
||||||
<Show when={summary}>
|
<div class="w-full flex flex-col gap-6 items-start self-stretch">
|
||||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
<div class="flex flex-col items-start gap-1 self-stretch">
|
||||||
<h2 class="text-12-medium text-text-weak">Summary</h2>
|
<h2 class="text-12-medium text-text-weak">Summary</h2>
|
||||||
<div class="text-14-regular text-text-base self-stretch">{summary()}</div>
|
<div class="text-14-regular text-text-base self-stretch">{summary()}</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
<Show when={message.summary?.diffs.length}>
|
|
||||||
<Accordion class="w-full" multiple>
|
<Accordion class="w-full" multiple>
|
||||||
<For each={message.summary?.diffs || []}>
|
<For each={message.summary?.diffs || []}>
|
||||||
{(diff) => (
|
{(diff) => (
|
||||||
@@ -735,22 +749,8 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
{/* Response */}
|
|
||||||
<div data-todo="Response" class="w-full">
|
|
||||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
|
||||||
<h2 class="text-12-medium text-text-weak">Response</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-col items-start self-stretch gap-8">
|
</Show>
|
||||||
<For each={assistantMessages()}>
|
|
||||||
{(assistantMessage) => {
|
|
||||||
const parts = createMemo(() => sync.data.part[assistantMessage.id])
|
|
||||||
return <AssistantMessage message={assistantMessage} parts={parts()} />
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -57,6 +57,27 @@
|
|||||||
/* animation: slideDown 250ms ease-out; */
|
/* animation: slideDown 250ms ease-out; */
|
||||||
/* } */
|
/* } */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-variant="ghost"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
> [data-slot="collapsible-trigger"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
/* &:hover { */
|
||||||
|
/* color: var(--text-strong); */
|
||||||
|
/* } */
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&[data-disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@keyframes slideDown {
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import { Icon } from "./icon"
|
|||||||
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
|
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
|
||||||
class?: string
|
class?: string
|
||||||
classList?: ComponentProps<"div">["classList"]
|
classList?: ComponentProps<"div">["classList"]
|
||||||
|
variant?: "normal" | "ghost"
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollapsibleRoot(props: CollapsibleProps) {
|
function CollapsibleRoot(props: CollapsibleProps) {
|
||||||
const [local, others] = splitProps(props, ["class", "classList"])
|
const [local, others] = splitProps(props, ["class", "classList", "variant"])
|
||||||
return (
|
return (
|
||||||
<Kobalte
|
<Kobalte
|
||||||
data-component="collapsible"
|
data-component="collapsible"
|
||||||
|
data-variant={local.variant || "normal"}
|
||||||
classList={{
|
classList={{
|
||||||
...(local.classList ?? {}),
|
...(local.classList ?? {}),
|
||||||
[local.class ?? ""]: !!local.class,
|
[local.class ?? ""]: !!local.class,
|
||||||
|
|||||||
@@ -48,6 +48,35 @@
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroller {
|
||||||
|
--fade-height: 1.5rem;
|
||||||
|
|
||||||
|
--mask-top: linear-gradient(to bottom, transparent, black var(--fade-height));
|
||||||
|
--mask-bottom: linear-gradient(to top, transparent, black var(--fade-height));
|
||||||
|
|
||||||
|
mask-image: var(--mask-top), var(--mask-bottom);
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
|
||||||
|
mask-size: 100% var(--fade-height);
|
||||||
|
|
||||||
|
animation: adjust-masks linear;
|
||||||
|
animation-timeline: scroll(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes adjust-masks {
|
||||||
|
from {
|
||||||
|
mask-position:
|
||||||
|
0 calc(0% - var(--fade-height)),
|
||||||
|
0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
mask-position:
|
||||||
|
0 0,
|
||||||
|
0 calc(100% + var(--fade-height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.truncate-start {
|
.truncate-start {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user