feat: add unit tests, Docker polish, and workbench UX improvements

Unit tests: Consumer class (7), recursive-splitter (10), parseJsonResponse (11) — 28 total.
Docker: add 5 commented LLM provider services, dev compose override, .env.example.
Workbench: chat persistence, error boundary, disconnect banner, prompts error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
elpresidank 2026-04-07 03:51:29 -05:00
parent c7eefee607
commit 72870a7e2e
17 changed files with 718 additions and 33 deletions

View file

@ -1,4 +1,5 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
// ---------------------------------------------------------------------------
// Types
@ -66,26 +67,38 @@ export function nextMessageId(): string {
return `msg-${++_nextMsgId}-${Date.now()}`;
}
export const useConversation = create<ConversationState>()((set) => ({
messages: [],
input: "",
chatMode: "graph-rag",
export const useConversation = create<ConversationState>()(
persist(
(set) => ({
messages: [],
input: "",
chatMode: "graph-rag",
setInput: (value) => set({ input: value }),
setChatMode: (mode) => set({ chatMode: mode }),
setInput: (value) => set({ input: value }),
setChatMode: (mode) => set({ chatMode: mode }),
addMessage: (message) =>
set((state) => ({ messages: [...state.messages, message] })),
addMessage: (message) =>
set((state) => ({ messages: [...state.messages, message] })),
updateLastMessage: (updater) =>
set((state) => {
if (state.messages.length === 0) return state;
const last = state.messages[state.messages.length - 1]!;
const updated = updater(last);
return {
messages: [...state.messages.slice(0, -1), updated],
};
updateLastMessage: (updater) =>
set((state) => {
if (state.messages.length === 0) return state;
const last = state.messages[state.messages.length - 1]!;
const updated = updater(last);
return {
messages: [...state.messages.slice(0, -1), updated],
};
}),
clearMessages: () => set({ messages: [] }),
}),
clearMessages: () => set({ messages: [] }),
}));
{
name: "tg-conversation",
// Only persist messages and chatMode, not input or transient state
partialize: (state) => ({
messages: state.messages.filter((m) => !m.isStreaming),
chatMode: state.chatMode,
}),
},
),
);

View file

@ -8,13 +8,17 @@ export function usePrompts() {
const [prompts, setPrompts] = useState<Array<{ id: string; name?: string; description?: string }>>([]);
const [systemPrompt, setSystemPrompt] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadPrompts = useCallback(async () => {
try {
setLoading(true);
setError(null);
const list = await socket.config().getPrompts();
setPrompts(Array.isArray(list) ? list : []);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(msg);
console.error("Failed to load prompts:", err);
} finally {
setLoading(false);
@ -46,5 +50,5 @@ export function usePrompts() {
}
}, [connectionState.status, loadPrompts, loadSystemPrompt]);
return { prompts, systemPrompt, loading, loadPrompts, loadSystemPrompt, getPrompt };
return { prompts, systemPrompt, loading, error, loadPrompts, loadSystemPrompt, getPrompt };
}