Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 0.3.0

This is a breaking change and requires the backend with version 0.3.x.

## What's new

* **[Breaking]** Support tool calling.
* **[Breaking]** Support file operations.

## Fixes

## Changes

# 0.2.0

This is a breaking change and requires the backend with version 0.2.x.
Expand Down
25 changes: 24 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,30 @@ const nextConfig: NextConfig = {
images: {
unoptimized: true,
},
/* other config options can go here */
// Allow loading the dev server from LAN IPs (HMR, RSC payloads, etc.).
// Without this, Next.js 15+ blocks dev resource requests when the page is
// accessed via anything other than localhost, causing hydration to silently
// fail and click handlers (e.g. the sign-in button) to do nothing.
allowedDevOrigins: ['192.168.31.153'],
// quickjs-emscripten's emscripten-generated code has require("fs") inside a
// Node.js-only branch that never executes in the browser. Stub it for both bundlers.
turbopack: {
resolveAlias: {
fs: './src/stubs/empty-module.ts',
},
},
webpack: (config) => {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
};
config.experiments = {
...config.experiments,
topLevelAwait: true,
};
return config;
},
};

export default nextConfig;
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinywebui-webapp",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -13,14 +13,16 @@
"@hpcc-js/wasm-zstd": "^1.11.0",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@tootallnate/quickjs-emscripten": "^0.23.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"github-markdown-css": "^5.8.1",
"katex": "^0.16.25",
"libsodium-wrappers-sumo": "0.7.15",
"lucide-react": "^0.536.0",
"next": "^15.5.6",
"next": "^16.2.4",
"next-themes": "^0.4.6",
"pdfjs-dist": "^5.7.284",
"postcss": "^8.5.4",
"prism-themes": "^1.9.0",
"react": "^19.0.0",
Expand Down
1 change: 0 additions & 1 deletion src/app/auth/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default function SignIn() {

if (!TUIClientSingleton.exists()) {
TUIClientSingleton.create(
/** `${window.location.hostname}:12345`, */
`${window.location.host}/api`,
(error) => {
/** @todo Handle disconnect properly */
Expand Down
117 changes: 117 additions & 0 deletions src/app/chat/assistant-turn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"use client";

import React, { useState } from "react";
import MarkdownRenderer from "@/components/custom/markdown-renderer";
import { ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
import type { FunctionCallMessage } from "@/sdk/types/IServer";

/** Parts that make up an assistant turn during generation. */
export type PendingTurnPart =
| { type: "text"; content: string }
| { type: "tool_call"; call: FunctionCallMessage; status: "calling" | "executing" | "done"; result?: string };

/**
* Parts for committed (already in history) assistant turns.
* Built from MessageNode groups between user messages.
*/
export type CommittedTurnPart =
| { type: "text"; content: string }
| { type: "tool_call"; call: FunctionCallMessage; result?: string };

function ToolCallSection({
call,
status,
result
}: {
call: FunctionCallMessage;
status?: "calling" | "executing" | "done";
result?: string;
}) {
const { name, arguments: args } = call;
const [expanded, setExpanded] = useState(false);
const isDone = status === undefined || status === "done";
const canExpand = isDone && (args || result);

const statusText = (() => {
switch (status) {
case "calling": return "调用中…";
case "executing": return "执行中…";
default: return undefined;
}
})();

return (
<div className="my-1 rounded border border-border/60 bg-muted/30 text-sm">
<button
type="button"
className={cn(
"flex w-full items-center gap-2 px-3 py-1.5 text-left text-muted-foreground",
canExpand && "cursor-pointer hover:bg-muted/50"
)}
onClick={() => canExpand && setExpanded(!expanded)}
disabled={!canExpand}
>
<ChevronRight
className={cn(
"size-3.5 shrink-0 transition-transform",
expanded && "rotate-90"
)}
/>
<span className="font-medium text-foreground/80">🔧 {name}</span>
{statusText && (
<span className="text-xs text-muted-foreground/70">{statusText}</span>
)}
</button>
{expanded && (
<div className="border-t border-border/40 px-3 py-2 space-y-2">
{args && (
<div>
<div className="text-xs font-medium text-muted-foreground mb-1">参数</div>
<pre className="text-xs whitespace-pre-wrap break-words bg-muted/40 rounded px-2 py-1 max-h-[200px] overflow-y-auto">
{(() => {
try { return JSON.stringify(JSON.parse(args), null, 2); } catch { return args; }
})()}
</pre>
</div>
)}
{result && (
<div>
<div className="text-xs font-medium text-muted-foreground mb-1">结果</div>
<pre className="text-xs whitespace-pre-wrap break-words bg-muted/40 rounded px-2 py-1 max-h-[300px] overflow-y-auto">
{result}
</pre>
</div>
)}
</div>
)}
</div>
);
}

/**
* Renders an assistant turn consisting of text and tool call parts.
* Used for both committed history and pending (streaming) turns.
*/
export function AssistantTurn({ parts }: { parts: (PendingTurnPart | CommittedTurnPart)[] }) {
return (
<div className="flex justify-start">
<div className="rounded-lg px-4 py-2 bg-background w-full min-w-0">
{parts.map((part, idx) => {
if (part.type === "text") {
if (!part.content) return null;
return <MarkdownRenderer key={idx} content={part.content} />;
}
return (
<ToolCallSection
key={idx}
call={part.call}
status={"status" in part ? part.status : undefined}
result={part.result}
/>
);
})}
</div>
</div>
);
}
Loading
Loading