|
@@ -1,5 +1,6 @@
|
|
|
import {
|
|
import {
|
|
|
AssistantMessage,
|
|
AssistantMessage,
|
|
|
|
|
+ FilePart,
|
|
|
Message as MessageType,
|
|
Message as MessageType,
|
|
|
Part as PartType,
|
|
Part as PartType,
|
|
|
type PermissionRequest,
|
|
type PermissionRequest,
|
|
@@ -29,6 +30,7 @@ import { Spinner } from "./spinner"
|
|
|
import { createStore } from "solid-js/store"
|
|
import { createStore } from "solid-js/store"
|
|
|
import { DateTime, DurationUnit, Interval } from "luxon"
|
|
import { DateTime, DurationUnit, Interval } from "luxon"
|
|
|
import { createAutoScroll } from "../hooks"
|
|
import { createAutoScroll } from "../hooks"
|
|
|
|
|
+import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
|
|
|
|
|
|
function computeStatusFromPart(part: PartType | undefined): string | undefined {
|
|
function computeStatusFromPart(part: PartType | undefined): string | undefined {
|
|
|
if (!part) return undefined
|
|
if (!part) return undefined
|
|
@@ -75,6 +77,12 @@ function same<T>(a: readonly T[], b: readonly T[]) {
|
|
|
return a.every((x, i) => x === b[i])
|
|
return a.every((x, i) => x === b[i])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function isAttachment(part: PartType | undefined) {
|
|
|
|
|
+ if (part?.type !== "file") return false
|
|
|
|
|
+ const mime = (part as FilePart).mime ?? ""
|
|
|
|
|
+ return mime.startsWith("image/") || mime === "application/pdf"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function AssistantMessageItem(props: {
|
|
function AssistantMessageItem(props: {
|
|
|
message: AssistantMessage
|
|
message: AssistantMessage
|
|
|
responsePartId: string | undefined
|
|
responsePartId: string | undefined
|
|
@@ -133,6 +141,7 @@ export function SessionTurn(
|
|
|
|
|
|
|
|
const emptyMessages: MessageType[] = []
|
|
const emptyMessages: MessageType[] = []
|
|
|
const emptyParts: PartType[] = []
|
|
const emptyParts: PartType[] = []
|
|
|
|
|
+ const emptyFiles: FilePart[] = []
|
|
|
const emptyAssistant: AssistantMessage[] = []
|
|
const emptyAssistant: AssistantMessage[] = []
|
|
|
const emptyPermissions: PermissionRequest[] = []
|
|
const emptyPermissions: PermissionRequest[] = []
|
|
|
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
|
|
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
|
|
@@ -180,6 +189,19 @@ export function SessionTurn(
|
|
|
return data.store.part[msg.id] ?? emptyParts
|
|
return data.store.part[msg.id] ?? emptyParts
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ const attachmentParts = createMemo(() => {
|
|
|
|
|
+ const msgParts = parts()
|
|
|
|
|
+ if (msgParts.length === 0) return emptyFiles
|
|
|
|
|
+ return msgParts.filter((part) => isAttachment(part)) as FilePart[]
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const stickyParts = createMemo(() => {
|
|
|
|
|
+ const msgParts = parts()
|
|
|
|
|
+ if (msgParts.length === 0) return emptyParts
|
|
|
|
|
+ if (attachmentParts().length === 0) return msgParts
|
|
|
|
|
+ return msgParts.filter((part) => !isAttachment(part))
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
const assistantMessages = createMemo(
|
|
const assistantMessages = createMemo(
|
|
|
() => {
|
|
() => {
|
|
|
const msg = message()
|
|
const msg = message()
|
|
@@ -331,6 +353,15 @@ export function SessionTurn(
|
|
|
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
|
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
|
|
|
|
|
|
|
const [responseCopied, setResponseCopied] = createSignal(false)
|
|
const [responseCopied, setResponseCopied] = createSignal(false)
|
|
|
|
|
+ const [rootRef, setRootRef] = createSignal<HTMLDivElement | undefined>()
|
|
|
|
|
+ const [stickyRef, setStickyRef] = createSignal<HTMLDivElement | undefined>()
|
|
|
|
|
+
|
|
|
|
|
+ const updateStickyHeight = (height: number) => {
|
|
|
|
|
+ const root = rootRef()
|
|
|
|
|
+ if (!root) return
|
|
|
|
|
+ const next = Math.ceil(height)
|
|
|
|
|
+ root.style.setProperty("--session-turn-sticky-height", `${next}px`)
|
|
|
|
|
+ }
|
|
|
const handleCopyResponse = async () => {
|
|
const handleCopyResponse = async () => {
|
|
|
const content = response()
|
|
const content = response()
|
|
|
if (!content) return
|
|
if (!content) return
|
|
@@ -361,6 +392,24 @@ export function SessionTurn(
|
|
|
onUserInteracted: props.onUserInteracted,
|
|
onUserInteracted: props.onUserInteracted,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ createResizeObserver(
|
|
|
|
|
+ () => stickyRef(),
|
|
|
|
|
+ ({ height }) => {
|
|
|
|
|
+ updateStickyHeight(height)
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ createEffect(() => {
|
|
|
|
|
+ const root = rootRef()
|
|
|
|
|
+ if (!root) return
|
|
|
|
|
+ const sticky = stickyRef()
|
|
|
|
|
+ if (!sticky) {
|
|
|
|
|
+ root.style.setProperty("--session-turn-sticky-height", "0px")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ updateStickyHeight(sticky.getBoundingClientRect().height)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
const diffInit = 20
|
|
const diffInit = 20
|
|
|
const diffBatch = 20
|
|
const diffBatch = 20
|
|
|
|
|
|
|
@@ -438,7 +487,7 @@ export function SessionTurn(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div data-component="session-turn" class={props.classes?.root}>
|
|
|
|
|
|
|
+ <div data-component="session-turn" class={props.classes?.root} ref={setRootRef}>
|
|
|
<div
|
|
<div
|
|
|
ref={autoScroll.scrollRef}
|
|
ref={autoScroll.scrollRef}
|
|
|
onScroll={autoScroll.handleScroll}
|
|
onScroll={autoScroll.handleScroll}
|
|
@@ -459,10 +508,15 @@ export function SessionTurn(
|
|
|
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
|
<Part part={shellModePart()!} message={msg()} defaultOpen />
|
|
|
</Match>
|
|
</Match>
|
|
|
<Match when={true}>
|
|
<Match when={true}>
|
|
|
- <div data-slot="session-turn-sticky">
|
|
|
|
|
|
|
+ <Show when={attachmentParts().length > 0}>
|
|
|
|
|
+ <div data-slot="session-turn-attachments">
|
|
|
|
|
+ <Message message={msg()} parts={attachmentParts()} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ <div data-slot="session-turn-sticky" ref={setStickyRef}>
|
|
|
{/* User Message */}
|
|
{/* User Message */}
|
|
|
<div data-slot="session-turn-message-content">
|
|
<div data-slot="session-turn-message-content">
|
|
|
- <Message message={msg()} parts={parts()} />
|
|
|
|
|
|
|
+ <Message message={msg()} parts={stickyParts()} />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Trigger (sticky) */}
|
|
{/* Trigger (sticky) */}
|