mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-25 03:34:22 +01:00
sync
This commit is contained in:
47
packages/web/src/components/CodeBlock.tsx
Normal file
47
packages/web/src/components/CodeBlock.tsx
Normal file
@@ -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<HTMLDivElement> {
|
||||
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 (
|
||||
<div ref={containerRef} {...rest}></div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeBlock
|
||||
73
packages/web/src/components/DiffView.tsx
Normal file
73
packages/web/src/components/DiffView.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { type Component, createSignal, onMount } from "solid-js"
|
||||
import { diffLines } 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<DiffViewProps> = (props) => {
|
||||
const [rows, setRows] = createSignal<DiffRow[]>([])
|
||||
|
||||
onMount(() => {
|
||||
const chunks = diffLines(props.oldCode, props.newCode)
|
||||
const diffRows: DiffRow[] = []
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const lines = chunk.value.split(/\r?\n/)
|
||||
if (lines.at(-1) === "") lines.pop()
|
||||
|
||||
for (const line of lines) {
|
||||
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 (
|
||||
<div class={`${styles.diff} ${props.class ?? ""}`}>
|
||||
<div class={styles.column}>
|
||||
{rows().map((r) => (
|
||||
<CodeBlock
|
||||
code={r.left}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={r.type === "removed" ? "removed" : ""}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class={styles.column}>
|
||||
{rows().map((r) => (
|
||||
<CodeBlock
|
||||
code={r.right}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={r.type === "added" ? "added" : ""}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiffView
|
||||
62
packages/web/src/components/Header.astro
Normal file
62
packages/web/src/components/Header.astro
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
import config from 'virtual:starlight/user-config';
|
||||
import { Icon } from '@astrojs/starlight/components';
|
||||
import { HeaderLinks } from 'toolbeam-docs-theme/components';
|
||||
import Default from 'toolbeam-docs-theme/overrides/Header.astro';
|
||||
import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro';
|
||||
|
||||
const path = Astro.url.pathname;
|
||||
|
||||
const links = config.social || [];
|
||||
---
|
||||
|
||||
{ path.startsWith("/share")
|
||||
? <div class="header sl-flex">
|
||||
<div class="title-wrapper sl-flex">
|
||||
<SiteTitle {...Astro.props} />
|
||||
</div>
|
||||
<div class="middle-group sl-flex">
|
||||
<HeaderLinks {...Astro.props} />
|
||||
</div>
|
||||
</div>
|
||||
: <Default {...Astro.props}><slot /></Default>
|
||||
}
|
||||
|
||||
<style>
|
||||
.header {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
/* Prevent long titles overflowing and covering the search and menu buttons on narrow viewports. */
|
||||
overflow: clip;
|
||||
/* Avoid clipping focus ring around link inside title wrapper. */
|
||||
padding: calc(0.25rem + 2px) 0.25rem calc(0.25rem - 2px);
|
||||
margin: -0.25rem;
|
||||
}
|
||||
|
||||
.middle-group {
|
||||
justify-content: flex-end;
|
||||
gap: var(--sl-nav-gap);
|
||||
}
|
||||
@media (max-width: 50rem) {
|
||||
:global(:root[data-has-sidebar]) {
|
||||
.middle-group {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (min-width: 50rem) {
|
||||
.middle-group {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style is:global>
|
||||
body > div.page > header {
|
||||
border-color: var(--sl-color-divider);
|
||||
}
|
||||
</style>
|
||||
|
||||
11
packages/web/src/components/Hero.astro
Normal file
11
packages/web/src/components/Hero.astro
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Default from '@astrojs/starlight/components/Hero.astro';
|
||||
import Lander from './Lander.astro';
|
||||
|
||||
const { slug } = Astro.locals.starlightRoute.entry;
|
||||
---
|
||||
|
||||
{ slug === ""
|
||||
? <Lander {...Astro.props} />
|
||||
: <Default {...Astro.props}><slot /></Default>
|
||||
}
|
||||
269
packages/web/src/components/Lander.astro
Normal file
269
packages/web/src/components/Lander.astro
Normal file
@@ -0,0 +1,269 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import config from "virtual:starlight/user-config";
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
|
||||
import CopyIcon from "../assets/lander/copy.svg";
|
||||
import CheckIcon from "../assets/lander/check.svg";
|
||||
|
||||
const { data } = Astro.locals.starlightRoute.entry;
|
||||
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
|
||||
|
||||
const imageAttrs = {
|
||||
loading: 'eager' as const,
|
||||
decoding: 'async' as const,
|
||||
width: 400,
|
||||
alt: image?.alt || '',
|
||||
};
|
||||
|
||||
const github = config.social.filter(s => s.icon === 'github')[0];
|
||||
|
||||
const command = "npm i -g";
|
||||
const pkg = "opencode";
|
||||
|
||||
let darkImage: ImageMetadata | undefined;
|
||||
let lightImage: ImageMetadata | undefined;
|
||||
let rawHtml: string | undefined;
|
||||
if (image) {
|
||||
if ('file' in image) {
|
||||
darkImage = image.file;
|
||||
} else if ('dark' in image) {
|
||||
darkImage = image.dark;
|
||||
lightImage = image.light;
|
||||
} else {
|
||||
rawHtml = image.html;
|
||||
}
|
||||
}
|
||||
---
|
||||
<div class="hero">
|
||||
<section class="top">
|
||||
<div class="logo">
|
||||
<Image
|
||||
src={darkImage}
|
||||
{...imageAttrs}
|
||||
class:list={{ 'light:sl-hidden': Boolean(lightImage) }}
|
||||
/>
|
||||
<Image src={lightImage} {...imageAttrs} class="dark:sl-hidden" />
|
||||
</div>
|
||||
<h1>The AI coding agent built for the terminal.</h1>
|
||||
</section>
|
||||
|
||||
<section class="cta">
|
||||
<div class="col1">
|
||||
<a href="/docs">View the docs</a>
|
||||
</div>
|
||||
<div class="col2">
|
||||
<button class="command" data-command={`${command} ${pkg}`}>
|
||||
<code>{command} <span class="highlight">{pkg}</span></code>
|
||||
<span class="copy">
|
||||
<CopyIcon />
|
||||
<CheckIcon />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col3">
|
||||
<a href={github.href}>Star on GitHub</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<ul>
|
||||
<li><b>Native TUI</b>: A native terminal UI for a smoother, snappier experience.</li>
|
||||
<li><b>LSP enabled</b>: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.</li>
|
||||
<li><b>Multi-session</b>: Start multiple conversations in a project to have agents working in parallel.</li>
|
||||
<li><b>Use any model</b>: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.</li>
|
||||
<li><b>Change tracking</b>: View the file changes from the current conversation in the sidebar.</li>
|
||||
<li><b>Edit with Vim</b>: Use Vim as an external editor to compose longer messages.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="footer">
|
||||
<div class="col1">
|
||||
<span>Version: Beta</span>
|
||||
</div>
|
||||
<div class="col2">
|
||||
<span>Author: <a href="https://sst.dev">SST</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
--padding: 3rem;
|
||||
--vertical-padding: 2rem;
|
||||
--heading-font-size: var(--sl-text-3xl);
|
||||
|
||||
margin: 1rem;
|
||||
border: 2px solid var(--sl-color-white);
|
||||
}
|
||||
@media (max-width: 30rem) {
|
||||
.hero {
|
||||
--padding: 1rem;
|
||||
--vertical-padding: 1rem;
|
||||
--heading-font-size: var(--sl-text-2xl);
|
||||
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.top {
|
||||
padding: var(--padding);
|
||||
|
||||
h1 {
|
||||
margin-top: calc(var(--vertical-padding) / 8);
|
||||
font-size: var(--heading-font-size);
|
||||
line-height: 1.25;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
width: clamp(200px, 70vw, 400px);
|
||||
}
|
||||
}
|
||||
|
||||
section.cta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
padding: calc(var(--padding) / 2) 0.5rem;
|
||||
}
|
||||
& > div:not(.col2) {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
& > div {
|
||||
padding-bottom: calc(var(--padding) / 2 + 4px);
|
||||
}
|
||||
}
|
||||
|
||||
& > div + div {
|
||||
border-left: 2px solid var(--sl-color-white);
|
||||
}
|
||||
|
||||
.command {
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
code {
|
||||
color: var(--sl-color-text-secondary);
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
code .highlight {
|
||||
color: var(--sl-color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.copy {
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
.copy svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.copy svg:first-child {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
.copy svg:last-child {
|
||||
color: var(--sl-color-text);
|
||||
display: none;
|
||||
}
|
||||
&.success .copy {
|
||||
pointer-events: none;
|
||||
}
|
||||
&.success .copy svg:first-child {
|
||||
display: none;
|
||||
}
|
||||
&.success .copy svg:last-child {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.content {
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
padding: var(--padding);
|
||||
|
||||
ul {
|
||||
padding-left: 1rem;
|
||||
|
||||
li + li {
|
||||
margin-top: calc(var(--vertical-padding) / 2);
|
||||
}
|
||||
|
||||
li b {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.approach {
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
padding: var(--padding);
|
||||
|
||||
p + p {
|
||||
margin-top: var(--vertical-padding);
|
||||
}
|
||||
}
|
||||
|
||||
section.footer {
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
padding: calc(var(--padding) / 2) 0.5rem;
|
||||
}
|
||||
|
||||
& > div + div {
|
||||
border-left: 2px solid var(--sl-color-white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
:root[data-has-hero] {
|
||||
header.header {
|
||||
display: none;
|
||||
}
|
||||
.main-frame {
|
||||
padding-top: 0;
|
||||
|
||||
.main-pane > main {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
main > .content-panel .sl-markdown-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const button = document.querySelector("button.command") as HTMLButtonElement;
|
||||
|
||||
button?.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(button.dataset.command!);
|
||||
button.classList.toggle("success");
|
||||
setTimeout(() => {
|
||||
button.classList.toggle("success");
|
||||
}, 1500);
|
||||
});
|
||||
</script>
|
||||
772
packages/web/src/components/Share.tsx
Normal file
772
packages/web/src/components/Share.tsx
Normal file
@@ -0,0 +1,772 @@
|
||||
import { type JSX } from "solid-js"
|
||||
import {
|
||||
For,
|
||||
Show,
|
||||
Match,
|
||||
Switch,
|
||||
onMount,
|
||||
onCleanup,
|
||||
splitProps,
|
||||
createMemo,
|
||||
createEffect,
|
||||
createSignal,
|
||||
} from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import {
|
||||
IconOpenAI,
|
||||
IconGemini,
|
||||
IconAnthropic,
|
||||
} from "./icons/custom"
|
||||
import {
|
||||
IconCpuChip,
|
||||
IconSparkles,
|
||||
IconUserCircle,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconPencilSquare,
|
||||
IconWrenchScrewdriver,
|
||||
} from "./icons"
|
||||
import DiffView from "./DiffView"
|
||||
import styles from "./share.module.css"
|
||||
import { type UIMessage } from "ai"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
|
||||
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
|
||||
|
||||
|
||||
type SessionMessage = UIMessage<{
|
||||
time: {
|
||||
created: number
|
||||
completed?: number
|
||||
}
|
||||
assistant?: {
|
||||
modelID: string;
|
||||
providerID: string;
|
||||
cost: number;
|
||||
tokens: {
|
||||
input: number;
|
||||
output: number;
|
||||
reasoning: number;
|
||||
};
|
||||
};
|
||||
sessionID: string
|
||||
tool: Record<string, {
|
||||
properties: Record<string, any>
|
||||
time: {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
}>
|
||||
}>
|
||||
|
||||
type SessionInfo = {
|
||||
title: string
|
||||
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]> = [];
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
entries.push(...flattenToolArgs(value, path));
|
||||
}
|
||||
else {
|
||||
entries.push([path, value]);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function getStatusText(status: [Status, string?]): string {
|
||||
switch (status[0]) {
|
||||
case "connected": return "Connected"
|
||||
case "connecting": return "Connecting..."
|
||||
case "disconnected": return "Disconnected"
|
||||
case "reconnecting": return "Reconnecting..."
|
||||
case "error": return status[1] || "Error"
|
||||
default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
function ProviderIcon(props: { provider: string, size?: number }) {
|
||||
const size = props.size || 16
|
||||
return (
|
||||
<Switch fallback={
|
||||
<IconSparkles width={size} height={size} />
|
||||
}>
|
||||
<Match when={props.provider === "openai"}>
|
||||
<IconOpenAI width={size} height={size} />
|
||||
</Match>
|
||||
<Match when={props.provider === "anthropic"}>
|
||||
<IconAnthropic width={size} height={size} />
|
||||
</Match>
|
||||
<Match when={props.provider === "gemini"}>
|
||||
<IconGemini width={size} height={size} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
interface ResultsButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
|
||||
results: boolean
|
||||
}
|
||||
function ResultsButton(props: ResultsButtonProps) {
|
||||
const [local, rest] = splitProps(props, ["results"])
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-element-button-text
|
||||
data-element-button-more
|
||||
{...rest}
|
||||
>
|
||||
<span>
|
||||
{local.results ? "Hide results" : "Show results"}
|
||||
</span>
|
||||
<span data-button-icon>
|
||||
<Show
|
||||
when={local.results}
|
||||
fallback={
|
||||
<IconChevronRight width={10} height={10} />
|
||||
}
|
||||
>
|
||||
<IconChevronDown width={10} height={10} />
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
text: string
|
||||
expand?: boolean
|
||||
highlight?: boolean
|
||||
}
|
||||
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 && !local.expand) {
|
||||
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkOverflow()
|
||||
window.addEventListener("resize", checkOverflow)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
local.text
|
||||
setTimeout(checkOverflow, 0)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", checkOverflow)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-element-message-text
|
||||
data-highlight={local.highlight}
|
||||
data-expanded={expanded() || local.expand === true}
|
||||
{...rest}
|
||||
>
|
||||
<pre ref={el => (preEl = el)}>{local.text}</pre>
|
||||
{overflowed() &&
|
||||
<button
|
||||
type="button"
|
||||
data-element-button-text
|
||||
onClick={() => setExpanded(e => !e)}
|
||||
>
|
||||
{expanded() ? "Show less" : "Show more"}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PartFooter(props: { time: number }) {
|
||||
return (
|
||||
<span
|
||||
data-part-footer
|
||||
title={
|
||||
DateTime.fromMillis(props.time).toLocaleString(
|
||||
DateTime.DATETIME_FULL_WITH_SECONDS
|
||||
)
|
||||
}
|
||||
>
|
||||
{DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Share(props: { api: string }) {
|
||||
let params = new URLSearchParams(document.location.search)
|
||||
const id = params.get("id")
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
info?: SessionInfo
|
||||
messages: Record<string, SessionMessage>
|
||||
}>({
|
||||
messages: {},
|
||||
})
|
||||
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
|
||||
|
||||
onMount(() => {
|
||||
const apiUrl = props.api
|
||||
|
||||
if (!id) {
|
||||
setConnectionStatus(["error", "id not found"])
|
||||
return
|
||||
}
|
||||
|
||||
if (!apiUrl) {
|
||||
console.error("API URL not found in environment variables")
|
||||
setConnectionStatus(["error", "API URL not found"])
|
||||
return
|
||||
}
|
||||
|
||||
let reconnectTimer: number | undefined
|
||||
let socket: WebSocket | null = null
|
||||
|
||||
// Function to create and set up WebSocket with auto-reconnect
|
||||
const setupWebSocket = () => {
|
||||
// Close any existing connection
|
||||
if (socket) {
|
||||
socket.close()
|
||||
}
|
||||
|
||||
setConnectionStatus(["connecting"])
|
||||
|
||||
// Always use secure WebSocket protocol (wss)
|
||||
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://")
|
||||
const wsUrl = `${wsBaseUrl}/share_poll?id=${id}`
|
||||
console.log("Connecting to WebSocket URL:", wsUrl)
|
||||
|
||||
// Create WebSocket connection
|
||||
socket = new WebSocket(wsUrl)
|
||||
|
||||
// Handle connection opening
|
||||
socket.onopen = () => {
|
||||
setConnectionStatus(["connected"])
|
||||
console.log("WebSocket connection established")
|
||||
}
|
||||
|
||||
// Handle incoming messages
|
||||
socket.onmessage = (event) => {
|
||||
console.log("WebSocket message received")
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
const [root, type, ...splits] = data.key.split("/")
|
||||
if (root !== "session") return
|
||||
if (type === "info") {
|
||||
setStore("info", reconcile(data.content))
|
||||
return
|
||||
}
|
||||
if (type === "message") {
|
||||
const [, messageID] = splits
|
||||
setStore("messages", messageID, reconcile(data.content))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing WebSocket message:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
socket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error)
|
||||
setConnectionStatus(["error", "Connection failed"])
|
||||
}
|
||||
|
||||
// Handle connection close and reconnection
|
||||
socket.onclose = (event) => {
|
||||
console.log(`WebSocket closed: ${event.code} ${event.reason}`)
|
||||
setConnectionStatus(["reconnecting"])
|
||||
|
||||
// Try to reconnect after 2 seconds
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = window.setTimeout(
|
||||
setupWebSocket,
|
||||
2000,
|
||||
) as unknown as number
|
||||
}
|
||||
}
|
||||
|
||||
// Initial connection
|
||||
setupWebSocket()
|
||||
|
||||
// Clean up on component unmount
|
||||
onCleanup(() => {
|
||||
console.log("Cleaning up WebSocket connection")
|
||||
if (socket) {
|
||||
socket.close()
|
||||
}
|
||||
clearTimeout(reconnectTimer)
|
||||
})
|
||||
})
|
||||
|
||||
const models = createMemo(() => {
|
||||
const result: string[][] = []
|
||||
for (const msg of messages()) {
|
||||
if (msg.role === "assistant" && msg.metadata?.assistant) {
|
||||
result.push([msg.metadata.assistant.providerID, msg.metadata.assistant.modelID])
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const metrics = createMemo(() => {
|
||||
const result = {
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
}
|
||||
}
|
||||
for (const msg of messages()) {
|
||||
const assistant = msg.metadata?.assistant
|
||||
if (!assistant) continue
|
||||
result.cost += assistant.cost
|
||||
result.tokens.input += assistant.tokens.input
|
||||
result.tokens.output += assistant.tokens.output
|
||||
result.tokens.reasoning += assistant.tokens.reasoning
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<main class={`${styles.root} not-content`}>
|
||||
<div class={styles.header}>
|
||||
<div data-section="title">
|
||||
<h1>{store.info?.title}</h1>
|
||||
<p>
|
||||
<span data-status={connectionStatus()[0]}>●</span>
|
||||
<span data-element-label>{getStatusText(connectionStatus())}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div data-section="row">
|
||||
<ul data-section="stats">
|
||||
<li>
|
||||
<span data-element-label>Cost</span>
|
||||
{metrics().cost !== undefined ?
|
||||
<span>${metrics().cost.toFixed(2)}</span>
|
||||
:
|
||||
<span data-placeholder>—</span>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<span data-element-label>Input Tokens</span>
|
||||
{metrics().tokens.input ?
|
||||
<span>{metrics().tokens.input}</span>
|
||||
:
|
||||
<span data-placeholder>—</span>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<span data-element-label>Output Tokens</span>
|
||||
{metrics().tokens.output ?
|
||||
<span>{metrics().tokens.output}</span>
|
||||
:
|
||||
<span data-placeholder>—</span>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<span data-element-label>Reasoning Tokens</span>
|
||||
{metrics().tokens.reasoning ?
|
||||
<span>{metrics().tokens.reasoning}</span>
|
||||
:
|
||||
<span data-placeholder>—</span>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
<ul data-section="stats" data-section-models>
|
||||
{models().length > 0 ?
|
||||
<For each={Array.from(models())}>
|
||||
{([provider, model]) => (
|
||||
<li>
|
||||
<div data-stat-model-icon title={provider}>
|
||||
<ProviderIcon provider={provider} />
|
||||
</div>
|
||||
<span data-stat-model>{model}</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
:
|
||||
<li>
|
||||
<span data-element-label>Models</span>
|
||||
<span data-placeholder>—</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div data-section="date">
|
||||
{messages().length > 0 && messages()[0].metadata?.time.created ?
|
||||
<span title={
|
||||
DateTime.fromMillis(
|
||||
messages()[0].metadata?.time.created || 0
|
||||
).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)
|
||||
}>
|
||||
{DateTime.fromMillis(
|
||||
messages()[0].metadata?.time.created || 0
|
||||
).toLocaleString(DateTime.DATE_MED)}
|
||||
</span>
|
||||
:
|
||||
<span data-element-label data-placeholder>Started at —</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Show
|
||||
when={messages().length > 0}
|
||||
fallback={<p>Waiting for messages...</p>}
|
||||
>
|
||||
<div class={styles.parts}>
|
||||
<For each={messages()}>
|
||||
{(msg, msgIndex) => (
|
||||
<For each={msg.parts}>
|
||||
{(part, partIndex) => {
|
||||
if (part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant)) return null
|
||||
|
||||
const [results, showResults] = createSignal(false)
|
||||
const isLastPart = createMemo(() =>
|
||||
(messages().length === msgIndex() + 1)
|
||||
&& (msg.parts.length === partIndex() + 1)
|
||||
)
|
||||
const time = msg.metadata?.time.completed
|
||||
|| msg.metadata?.time.created
|
||||
|| 0
|
||||
return (
|
||||
<Switch>
|
||||
{ /* User text */}
|
||||
<Match when={
|
||||
msg.role === "user" && part.type === "text" && part
|
||||
}>
|
||||
{part =>
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="user-text"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div>
|
||||
<IconUserCircle width={18} height={18} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<TextPart
|
||||
highlight
|
||||
text={part().text}
|
||||
expand={isLastPart()}
|
||||
/>
|
||||
<PartFooter time={time} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Match>
|
||||
{ /* AI text */}
|
||||
<Match when={
|
||||
msg.role === "assistant"
|
||||
&& part.type === "text"
|
||||
&& part
|
||||
}>
|
||||
{part =>
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="ai-text"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div><IconSparkles width={18} height={18} /></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<TextPart
|
||||
text={part().text}
|
||||
expand={isLastPart()}
|
||||
/>
|
||||
<PartFooter time={time} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Match>
|
||||
{ /* AI model */}
|
||||
<Match when={
|
||||
msg.role === "assistant"
|
||||
&& part.type === "step-start"
|
||||
&& msg.metadata?.assistant
|
||||
}>
|
||||
{assistant =>
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="ai-model"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div>
|
||||
<ProviderIcon
|
||||
size={18}
|
||||
provider={assistant().providerID}
|
||||
/>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<div data-part-tool-body>
|
||||
<span
|
||||
data-size="md"
|
||||
data-part-title
|
||||
data-element-label
|
||||
>
|
||||
{assistant().providerID}
|
||||
</span>
|
||||
<span data-part-model>
|
||||
{assistant().modelID}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Match>
|
||||
{ /* System text */}
|
||||
<Match when={
|
||||
msg.role === "system"
|
||||
&& part.type === "text"
|
||||
&& part
|
||||
}>
|
||||
{part =>
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="system-text"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div>
|
||||
<IconCpuChip width={18} height={18} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<div data-part-tool-body>
|
||||
<span data-element-label data-part-title>
|
||||
System
|
||||
</span>
|
||||
<TextPart
|
||||
data-size="sm"
|
||||
text={part().text}
|
||||
data-color="dimmed"
|
||||
/>
|
||||
</div>
|
||||
<PartFooter time={time} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Match>
|
||||
{ /* Edit tool */}
|
||||
<Match when={
|
||||
msg.role === "assistant"
|
||||
&& part.type === "tool-invocation"
|
||||
&& part.toolInvocation.toolName === "edit"
|
||||
&& part
|
||||
}>
|
||||
{part => {
|
||||
const args = part().toolInvocation.args
|
||||
const filePath = args.filePath
|
||||
return (
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="tool-edit"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div>
|
||||
<IconPencilSquare width={18} height={18} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<div data-part-tool-body>
|
||||
<span data-part-title data-size="md">
|
||||
Edit {filePath}
|
||||
</span>
|
||||
<div data-part-tool-edit>
|
||||
<DiffView
|
||||
class={styles["code-block"]}
|
||||
oldCode={args.oldString}
|
||||
newCode={args.newString}
|
||||
lang={getFileType(filePath)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PartFooter time={time} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Match>
|
||||
{ /* Tool call */}
|
||||
<Match when={
|
||||
msg.role === "assistant"
|
||||
&& part.type === "tool-invocation"
|
||||
&& part
|
||||
}>
|
||||
{part =>
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="tool-fallback"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div>
|
||||
<IconWrenchScrewdriver width={18} height={18} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<div data-part-tool-body>
|
||||
<span data-part-title data-size="md">
|
||||
{part().toolInvocation.toolName}
|
||||
</span>
|
||||
<div data-part-tool-args>
|
||||
<For each={
|
||||
flattenToolArgs(part().toolInvocation.args)
|
||||
}>
|
||||
{([name, value]) =>
|
||||
<>
|
||||
<div></div>
|
||||
<div>{name}</div>
|
||||
<div>{value}</div>
|
||||
</>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={
|
||||
part().toolInvocation.state === "result"
|
||||
&& part().toolInvocation.result
|
||||
}>
|
||||
<div data-part-tool-result>
|
||||
<ResultsButton
|
||||
results={results()}
|
||||
onClick={() => showResults(e => !e)}
|
||||
/>
|
||||
<Show when={results()}>
|
||||
<TextPart
|
||||
expand
|
||||
data-size="sm"
|
||||
data-color="dimmed"
|
||||
text={part().toolInvocation.result}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={
|
||||
part().toolInvocation.state === "call"
|
||||
}>
|
||||
<TextPart
|
||||
data-size="sm"
|
||||
data-color="dimmed"
|
||||
text="Calling..."
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<PartFooter time={time} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Match>
|
||||
{ /* Fallback */}
|
||||
<Match when={true}>
|
||||
<div
|
||||
data-section="part"
|
||||
data-part-type="fallback"
|
||||
>
|
||||
<div data-section="decoration">
|
||||
<div>
|
||||
<Switch fallback={
|
||||
<IconWrenchScrewdriver width={16} height={16} />
|
||||
}>
|
||||
<Match when={msg.role === "assistant" && part.type !== "tool-invocation"}>
|
||||
<IconSparkles width={18} height={18} />
|
||||
</Match>
|
||||
<Match when={msg.role === "system"}>
|
||||
<IconCpuChip width={18} height={18} />
|
||||
</Match>
|
||||
<Match when={msg.role === "user"}>
|
||||
<IconUserCircle width={18} height={18} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<div data-part-tool-body>
|
||||
<span data-element-label data-part-title>
|
||||
{part.type}
|
||||
</span>
|
||||
<TextPart text={JSON.stringify(part, null, 2)} />
|
||||
</div>
|
||||
<PartFooter time={time} />
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div style={{ margin: "2rem 0" }}>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
padding: "1rem",
|
||||
"overflow-y": "auto",
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={messages().length > 0}
|
||||
fallback={<p>Waiting for messages...</p>}
|
||||
>
|
||||
<ul style={{ "list-style-type": "none", padding: 0 }}>
|
||||
<For each={messages()}>
|
||||
{(msg) => (
|
||||
<li
|
||||
style={{
|
||||
padding: "0.75rem",
|
||||
margin: "0.75rem 0",
|
||||
"box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong>Key:</strong> {msg.id}
|
||||
</div>
|
||||
<pre>{JSON.stringify(msg, null, 2)}</pre>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</main >
|
||||
)
|
||||
}
|
||||
80
packages/web/src/components/diffview.module.css
Normal file
80
packages/web/src/components/diffview.module.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.diff {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
background-color: var(--sl-color-bg-surface);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid var(--sl-color-divider);
|
||||
}
|
||||
|
||||
& > [data-section="cell"]:first-child {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
& > [data-section="cell"]:last-child {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="cell"] {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: max-content;
|
||||
padding: 0.1875rem 0.5rem 0.1875rem 1.8ch;
|
||||
margin: 0;
|
||||
|
||||
pre {
|
||||
background-color: var(--sl-color-bg-surface) !important;
|
||||
white-space: pre;
|
||||
|
||||
code > span:empty::before {
|
||||
content: "\00a0";
|
||||
white-space: pre;
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-diff-type="removed"] {
|
||||
background-color: var(--sl-color-red-low);
|
||||
min-width: 100%;
|
||||
|
||||
pre {
|
||||
background-color: var(--sl-color-red-low) !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);
|
||||
min-width: 100%;
|
||||
|
||||
pre {
|
||||
background-color: var(--sl-color-green-low) !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "+";
|
||||
position: absolute;
|
||||
left: 0.6ch;
|
||||
user-select: none;
|
||||
color: var(--sl-color-green-high);
|
||||
}
|
||||
}
|
||||
22
packages/web/src/components/icons/custom.tsx
Normal file
22
packages/web/src/components/icons/custom.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type JSX } from "solid-js"
|
||||
|
||||
// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill
|
||||
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill
|
||||
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill
|
||||
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z" /></svg>
|
||||
)
|
||||
}
|
||||
6101
packages/web/src/components/icons/index.tsx
Normal file
6101
packages/web/src/components/icons/index.tsx
Normal file
File diff suppressed because one or more lines are too long
326
packages/web/src/components/share.module.css
Normal file
326
packages/web/src/components/share.module.css
Normal file
@@ -0,0 +1,326 @@
|
||||
.root {
|
||||
padding-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
[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-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;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
[data-section="title"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
[data-section="row"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.125;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
p {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
|
||||
span:first-child {
|
||||
color: var(--sl-color-divider);
|
||||
|
||||
&[data-status="connected"] { color: var(--sl-color-green); }
|
||||
&[data-status="connecting"] { color: var(--sl-color-orange); }
|
||||
&[data-status="disconnected"] { color: var(--sl-color-divider); }
|
||||
&[data-status="reconnecting"] { color: var(--sl-color-orange); }
|
||||
&[data-status="error"] { color: var(--sl-color-red); }
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="stats"] {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
span[data-placeholder] {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="stats"][data-section-models] {
|
||||
li {
|
||||
gap: 0.3125rem;
|
||||
|
||||
[data-stat-model-icon] {
|
||||
flex: 0 0 auto;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
opacity: 0.85;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
span[data-stat-model] {
|
||||
color: var(sl-color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="date"] {
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
color: var(--sl-color-text);
|
||||
|
||||
&[data-placeholder] {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
|
||||
[data-section="part"] {
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
[data-section="decoration"] {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
div:first-child {
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
svg {
|
||||
color: var(--sl-color-text-secondary);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background-color: var(--sl-color-hairline);
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="content"] {
|
||||
padding: 0 0 0.375rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
[data-part-tool-body] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
span[data-part-title] {
|
||||
line-height: 18px;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&[data-size="md"] {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
span[data-part-footer] {
|
||||
align-self: flex-start;
|
||||
font-size: 0.75rem;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
|
||||
span[data-part-model] {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-part-tool-args] {
|
||||
display: inline-grid;
|
||||
align-items: center;
|
||||
grid-template-columns: max-content max-content minmax(0, 1fr);
|
||||
max-width: 100%;
|
||||
gap: 0.25rem 0.375rem;
|
||||
|
||||
|
||||
& > div:nth-child(3n+1) {
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: var(--sl-color-divider);
|
||||
}
|
||||
|
||||
& > div:nth-child(3n+2),
|
||||
& > div:nth-child(3n+3) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
& > div:nth-child(3n+3) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-element-message-text] {
|
||||
background-color: var(--sl-color-bg-surface);
|
||||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
|
||||
pre {
|
||||
line-height: 1.5;
|
||||
font-size: 0.875rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--sl-color-text);
|
||||
}
|
||||
|
||||
&[data-size="sm"] {
|
||||
pre {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-color="dimmed"] {
|
||||
pre {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
&[data-highlight="true"] {
|
||||
background-color: var(--sl-color-blue-high);
|
||||
|
||||
pre {
|
||||
color: var(--sl-color-text-invert);
|
||||
}
|
||||
|
||||
button {
|
||||
opacity: 0.85;
|
||||
color: var(--sl-color-text-invert);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-expanded="true"] {
|
||||
pre {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&[data-expanded="false"] {
|
||||
pre {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-block {
|
||||
pre {
|
||||
line-height: 1.25;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user