Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7c96096
feat(tauri): add macOS DMG configuration and background image
Loosand Aug 19, 2026
20811b8
Merge branch 'Stack-Cairn:main' into main
Loosand Aug 20, 2026
162bbee
feat(macos): Enhance DMG build process with new settings and verifica…
Loosand Aug 20, 2026
0ffabf3
Merge branch 'Stack-Cairn:main' into main
Loosand Aug 21, 2026
cb40e24
Merge branch 'main' of https://github.com/Loosand/LiveAgent
Loosand Aug 25, 2026
beea7a1
Add chat UI gallery
Loosand Aug 25, 2026
0819900
Add new chat UI components: ApprovalCard, CodeBlock, ContextCards, Di…
Loosand Aug 25, 2026
6879600
Refactor chat gallery components and styles for improved layout and f…
Loosand Aug 25, 2026
33ee1a5
Refine chat markdown typography
Loosand Aug 25, 2026
c9c73b4
Remove conversation loading scan animation
Loosand Aug 25, 2026
47cf47f
Compact assistant message metadata
Loosand Aug 25, 2026
0a799ec
Match Tessera reasoning and search UI
Loosand Aug 25, 2026
c9111f9
Merge branch 'Stack-Cairn:main' into codex/chat-ui-gallery
Loosand Aug 25, 2026
976ee67
feat(chat): add TaskRows component for task progress visualization
Loosand Aug 25, 2026
290796e
fix(tests): update assertions in AskUserQuestionCard and AssistantSta…
Loosand Aug 25, 2026
f11be26
feat(chat): enhance ToolCallItem and ToolTraceGroup components with c…
Loosand Aug 25, 2026
79df9ea
refactor(chat): improve ToolCallItem result display logic
Loosand Aug 25, 2026
529fe80
refactor(chat): streamline GatewayTranscript component and enhance st…
Loosand Aug 26, 2026
9daea95
refactor(chat): enhance interaction handling in AssistantWorkTrace an…
Loosand Aug 26, 2026
09854f8
test(chat): add tests for activity disclosure state and mention popup…
Loosand Aug 26, 2026
d52120f
refactor(chat): enhance TaskProgressIndicator with panel toggle funct…
Loosand Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
389 changes: 389 additions & 0 deletions .chatui/approval-card.tsx

Large diffs are not rendered by default.

191 changes: 191 additions & 0 deletions .chatui/code-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"use client";

import { useCallback, useState, type ReactNode } from "react";

/* ─────────────────────────────────────────────────────────
* CODE BLOCK
* A light editor panel with two versions (switch in the card):
* · Code — a line-numbered listing
* · Diff — a unified diff: old/new gutters, a green/red accent
* bar and row tint, plus word-level add/del highlights.
* Both share syntax coloring, insets, and wrapping behavior.
* ───────────────────────────────────────────────────────── */

const FILE = "churn.ts";

const CODE_LINES = [
"export async function churnBatch() {",
' const flavor = await getFlavor("pistachio");',
" const base = await dairy.fetch({ flavor });",
' await freezer.store(base, { temp: "-16C" });',
" if (!base.approved) return null;",
" return base.gallons;",
"}",
];
const RAW = CODE_LINES.join("\n");

type Piece = { text: string; change?: "add" | "del" };
type Row = { old: number | null; cur: number | null; type: "ctx" | "add" | "del"; pieces: Piece[] };

const DIFF: Row[] = [
{ old: 1, cur: 1, type: "ctx", pieces: [{ text: "export async function churnBatch() {" }] },
{ old: 2, cur: 2, type: "ctx", pieces: [{ text: ' const flavor = await getFlavor("pistachio");' }] },
{ old: 3, cur: 3, type: "ctx", pieces: [{ text: " const base = await dairy.fetch({ flavor });" }] },
{ old: 4, cur: null, type: "del", pieces: [{ text: " await freezer.store(base, { temp: " }, { text: '"-14C"', change: "del" }, { text: " });" }] },
{ old: null, cur: 4, type: "add", pieces: [{ text: " await freezer.store(base, { temp: " }, { text: '"-16C"', change: "add" }, { text: " });" }] },
{ old: null, cur: 5, type: "add", pieces: [{ text: " if (!base.approved) return null;" }] },
{ old: 5, cur: 6, type: "ctx", pieces: [{ text: " return base.gallons;" }] },
{ old: 6, cur: 7, type: "ctx", pieces: [{ text: "}" }] },
];

const HATCH = "repeating-linear-gradient(45deg, var(--red) 0, var(--red) 1.5px, transparent 1.5px, transparent 3px)";

/* light syntax coloring — keywords/imports/conditionals, functions, strings & numbers */
const KEYWORDS = new Set(["import", "from", "export", "default", "async", "function", "const", "let", "var", "await", "return", "if", "else", "for", "while", "new", "throw", "try", "catch", "null", "true", "false", "undefined"]);
const TOKEN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[^`]*`|\b\d+(?:\.\d+)?\b|\b(?:import|from|export|default|async|function|const|let|var|await|return|if|else|for|while|new|throw|try|catch|null|true|false|undefined)\b|[A-Za-z_$][\w$]*(?=\s*\())/g;

function highlight(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
let last = 0;
let k = 0;
for (const m of text.matchAll(TOKEN)) {
const idx = m.index ?? 0;
const t = m[0];
if (idx > last) nodes.push(<span key={k++}>{text.slice(last, idx)}</span>);
let color: string;
let weight: number | undefined;
if (/^["'`]/.test(t) || /^\d/.test(t)) color = "var(--orange)"; // string / number
else if (KEYWORDS.has(t)) color = "var(--accent-ink)"; // keyword / import / conditional
else { color = "var(--ink)"; weight = 500; } // function call
nodes.push(<span key={k++} style={{ color, fontWeight: weight }}>{t}</span>);
last = idx + t.length;
}
if (last < text.length) nodes.push(<span key={k++}>{text.slice(last)}</span>);
return nodes;
}

function Pieces({ pieces }: { pieces: Piece[] }) {
return (
<>
{pieces.map((p, i) => {
if (p.change) {
const add = p.change === "add";
return (
<span
key={i}
className="rounded-[3px]"
style={{
background: `color-mix(in srgb, var(--${add ? "green" : "red"}) 18%, transparent)`,
padding: "0 2px",
margin: "0 -1px",
boxDecorationBreak: "clone",
WebkitBoxDecorationBreak: "clone",
}}
>
{highlight(p.text)}
</span>
);
}
return <span key={i}>{highlight(p.text)}</span>;
})}
</>
);
}

function FileIcon() {
return (
<svg aria-hidden width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" className="shrink-0 text-ink-3">
<path d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" />
</svg>
);
}

export default function CodeBlock({ variant = "Code" }: { variant?: string }) {
const [copied, setCopied] = useState(false);
const isDiff = variant === "Diff";

const copy = useCallback(() => {
navigator.clipboard.writeText(RAW).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
}, []);

const added = DIFF.filter((r) => r.type === "add").length;
const removed = DIFF.filter((r) => r.type === "del").length;

return (
<div className="w-full max-w-105 overflow-hidden rounded-card bg-surface shadow-card">
{/* header — file · (diff stat | copy) */}
<div className="flex h-11 items-center gap-2 border-b border-line px-4 text-[12.5px]">
<span className="inline-flex min-w-0 items-center gap-[7px]">
<FileIcon />
<span className="truncate font-mono leading-none text-ink">{FILE}</span>
</span>

{isDiff ? (
<span className="ml-auto inline-flex items-center gap-2 font-mono text-[12px] leading-none tabular-nums">
<span className="text-green">+{added}</span>
<span className="text-red">-{removed}</span>
</span>
) : (
<button
type="button"
aria-label="Copy code"
onClick={copy}
className={`-mr-1 ml-auto flex h-6 items-center gap-1 rounded-[6px] px-1.5 text-[12px]
font-medium transition-colors duration-100 hover:bg-hover
${copied ? "text-green" : "text-ink-3 hover:text-ink"}`}
>
{copied ? (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>
) : (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="12" height="12" rx="2.5" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>
)}
{copied ? "Copied" : "Copy"}
</button>
)}
</div>

{/* body — equal 12px inset on top / left / right; lines wrap */}
<div className="py-3 font-mono text-[12.5px] leading-[1.65] text-ink-2">
{isDiff ? (
<div className="relative">
<span className="pointer-events-none absolute inset-y-0 left-5 w-px bg-line" />
{DIFF.map((r, i) => {
const add = r.type === "add";
const del = r.type === "del";
// one gutter column: removals keep the old number, additions/context show the new one
const num = del ? r.old : r.cur;
return (
<div
key={i}
className={`relative grid grid-cols-[20px_minmax(0,1fr)] items-start
${add ? "bg-green-tint" : del ? "bg-red-tint" : ""}`}
>
{(add || del) && (
<span className="absolute inset-y-0 left-0 w-[3px]" style={{ background: add ? "var(--green)" : HATCH }} />
)}
<span className={`select-none text-center text-[11px] ${add ? "text-green" : del ? "text-red" : "text-ink-3"}`}>{num ?? ""}</span>
<code className="pr-3 pl-1 break-words whitespace-pre-wrap">
<Pieces pieces={r.pieces} />
</code>
</div>
);
})}
</div>
) : (
<div className="relative">
<span className="pointer-events-none absolute inset-y-0 left-5 w-px bg-line" />
{CODE_LINES.map((line, i) => (
<div key={i} className="grid grid-cols-[20px_minmax(0,1fr)] items-start">
<span className="select-none text-center text-[11px] text-ink-3">{i + 1}</span>
<code className="pr-3 pl-1 break-words whitespace-pre-wrap">{highlight(line)}</code>
</div>
))}
</div>
)}
</div>
</div>
);
}
90 changes: 90 additions & 0 deletions .chatui/context-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { useEffect, useState } from "react";

/* ─────────────────────────────────────────────────────────
* CONTEXT CARDS
* Retrieved chunks enter once, then remain available.
* ───────────────────────────────────────────────────────── */

const CHUNKS = [
{
title: "Vendor onboarding rule",
chars: "290 characters",
body: "Cold-chain certification must be verified before a new dairy can be added to the reorder workflow.",
source: "Dairy Onboarding SOP.pdf",
badge: "PDF",
tone: "bg-red",
},
{
title: "Seasonal demand row",
chars: "1,250 characters",
body: "Q4 velocity table: pistachio +18%, vanilla +6%, rocky road -11%; retire flavors below 40 scoops weekly.",
source: "Sales Velocity Export.csv",
badge: "CSV",
tone: "bg-green",
},
];

export default function ContextCards() {
const [chipsShown, setChipsShown] = useState(false);

useEffect(() => {
const chips = setTimeout(() => setChipsShown(true), 700);
return () => clearTimeout(chips);
}, []);

return (
<div className="flex w-full max-w-95 flex-col gap-2">
<div
className="flex items-center gap-2 px-0.5"
style={{ animation: "fade-in 400ms ease-out both" }}
>
<span className="text-[13px] font-semibold text-ink">All chunks</span>
<span className="inline-flex h-5 items-center rounded-md bg-inset px-1.5 text-[11.5px] font-medium text-ink-2 shadow-hairline tabular-nums">
32
</span>
</div>

{CHUNKS.map((chunk, i) => (
<div
key={chunk.title}
className="overflow-hidden rounded-card bg-surface shadow-card"
style={{
animation: `fade-up 400ms cubic-bezier(0.23,1,0.32,1) ${i * 100}ms both`,
}}
>
<div className="primitive-card-bar flex items-center gap-2.5 border-b border-line">
<span className="flex min-w-0 items-center gap-1.5 text-[13px] font-medium text-ink">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M4 6h16M4 12h16M4 18h10" /></svg>
<span className="truncate">{chunk.title}</span>
</span>
<span className="ml-auto shrink-0 text-[12px] text-ink-3 tabular-nums">{chunk.chars}</span>
</div>
<p className="px-3 pt-2 pb-1 text-[12.5px] leading-relaxed text-ink-2">
{chunk.body}
</p>
<div className="px-3 pb-3">
<span
className="inline-flex h-6 items-center gap-1.5 rounded-full bg-inset px-2
text-[12px] font-medium text-ink-2 shadow-btn
transition-[opacity,transform,background-color] duration-300 hover:bg-hover"
style={{
opacity: chipsShown ? 1 : 0,
transform: chipsShown ? "scale(1)" : "scale(0.95)",
transitionTimingFunction: "cubic-bezier(0.23, 1, 0.32, 1)",
transitionDelay: `${i * 80}ms`,
}}
>
<span className={`flex size-3.5 items-center justify-center rounded-[4px] ${chunk.tone} text-[7px] font-bold text-white`}>
{chunk.badge}
</span>
{chunk.source}
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17L17 7M7 7h10v10" /></svg>
</span>
</div>
</div>
))}
</div>
);
}
Loading
Loading