Compare commits

..

No commits in common. "d3a841d6a970666343376762f79bd53bc930789d" and "fe09c70e7556abee06c5f6b62561529624277ffb" have entirely different histories.

7 changed files with 50 additions and 41 deletions

View File

@ -1,7 +1,8 @@
import { sendCommand } from "./client";
import { pickCase } from "./normalize";
//#region Public Types
// ── Public types ────────────────────────────────────────────────
export type AiHealthDto = {
provider: string;
enabled: boolean;
@ -44,9 +45,9 @@ export type CoachSessionPayload = {
recentFragments?: string[];
preferences?: CoachPreferencesDto;
};
//#endregion
//#region PascalCase Normalizers
// ── Raw (PascalCase) variants for normalization ─────────────────
type AiHealthDtoRaw = {
provider?: string;
enabled?: boolean;
@ -92,9 +93,9 @@ type CoachPlanDtoRaw = {
Evidence?: CoachEvidenceDtoRaw[];
PatchProposal?: CoachPatchProposalDtoRaw | null;
};
//#endregion
//#region Normalizers
// ── Normalizers ─────────────────────────────────────────────────
function normalizeHealth(raw: AiHealthDtoRaw): AiHealthDto {
return {
provider: pickCase(raw, "provider", "Provider", ""),
@ -162,9 +163,9 @@ function normalizeCoachPlan(raw: CoachPlanDtoRaw): CoachPlanDto {
patchProposal: normalizePatchProposal(patchRaw),
};
}
//#endregion
//#region API Functions
// ── API functions ───────────────────────────────────────────────
export async function aiHealth(): Promise<AiHealthDto> {
const data = await sendCommand<AiHealthDtoRaw>({
action: "ai.health",
@ -219,4 +220,3 @@ export async function coachWeekly(
});
return normalizeCoachPlan(data);
}
//#endregion

View File

@ -1,7 +1,8 @@
import { sendCommand } from "./client";
import { pickCase } from "./normalize";
//#region Public Types
// ── Public types ────────────────────────────────────────────────
export type ConversationDto = {
id: string;
title: string;
@ -28,9 +29,9 @@ export type ConversationChatResult = {
userMessage: ConversationMessageDto;
assistantMessage: ConversationMessageDto;
};
//#endregion
//#region PascalCase Normalizers
// ── Raw (PascalCase) variants ───────────────────────────────────
type ConversationDtoRaw = {
id?: string;
title?: string;
@ -72,9 +73,9 @@ type ConversationChatResultRaw = {
UserMessage?: ConversationMessageDtoRaw;
AssistantMessage?: ConversationMessageDtoRaw;
};
//#endregion
//#region Normalizers
// ── Normalizers ─────────────────────────────────────────────────
function normalizeMessage(
raw: ConversationMessageDtoRaw,
): ConversationMessageDto {
@ -131,9 +132,9 @@ function normalizeChatResult(
assistantMessage: normalizeMessage(assistantRaw),
};
}
//#endregion
//#region API Functions
// ── API functions ───────────────────────────────────────────────
export async function listConversations(): Promise<ConversationDto[]> {
const data = await sendCommand<ConversationDtoRaw[]>({
action: "conversations.list",
@ -192,4 +193,3 @@ export async function conversationChat(
});
return normalizeChatResult(data);
}
//#endregion

View File

@ -1279,7 +1279,6 @@
{#each $conversationsStore.items as conv}
<li class:is-active={conv.id === $activeConversationStore.id}>
{#if editingConversationId === conv.id}
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
class="rename-input"

View File

@ -10,7 +10,8 @@ import {
type CoachSessionPayload,
} from "$lib/backend/ai";
//#region Store Shapes
// ── Store shapes ────────────────────────────────────────────────
type AiStatusState = {
checking: boolean;
health: AiHealthDto | null;
@ -32,9 +33,9 @@ type ChatState = {
busy: boolean;
messages: ChatMessage[];
};
//#endregion
//#region Stores
// ── Stores ──────────────────────────────────────────────────────
export const aiStatusStore = writable<AiStatusState>({
checking: false,
health: null,
@ -51,9 +52,9 @@ export const chatStateStore = writable<ChatState>({
busy: false,
messages: [],
});
//#endregion
//#region Actions
// ── Actions ─────────────────────────────────────────────────────
export async function checkAiHealth(): Promise<void> {
aiStatusStore.update((s) => ({ ...s, checking: true }));
try {
@ -137,4 +138,3 @@ export function clearCoachPlan(): void {
export function clearChat(): void {
chatStateStore.set({ busy: false, messages: [] });
}
//#endregion

View File

@ -10,7 +10,8 @@ import {
type ConversationMessageDto,
} from "$lib/backend/conversations";
//#region Store Shapes
// ── Store shapes ────────────────────────────────────────────────
type ConversationsState = {
items: ConversationDto[];
busy: boolean;
@ -24,9 +25,9 @@ type ActiveConversationState = {
busy: boolean;
error: string;
};
//#endregion
//#region Stores
// ── Stores ──────────────────────────────────────────────────────
export const conversationsStore = writable<ConversationsState>({
items: [],
busy: false,
@ -40,9 +41,9 @@ export const activeConversationStore = writable<ActiveConversationState>({
busy: false,
error: "",
});
//#endregion
//#region Actions
// ── Actions ─────────────────────────────────────────────────────
export async function loadConversations(): Promise<void> {
conversationsStore.update((s) => ({ ...s, busy: true, error: "" }));
try {
@ -66,6 +67,7 @@ export async function createNewConversation(
...s,
items: [conv, ...s.items],
}));
// Auto-open the new conversation
activeConversationStore.set({
id: conv.id,
title: conv.title,
@ -111,6 +113,7 @@ export async function openConversation(id: string): Promise<void> {
export async function sendConversationMessage(prompt: string): Promise<void> {
const state = get(activeConversationStore);
if (!state.id) {
// Auto-create a conversation from the first message
const title = prompt.length > 40 ? prompt.slice(0, 37) + "..." : prompt;
const convId = await createNewConversation(title);
if (!convId) return;
@ -119,6 +122,7 @@ export async function sendConversationMessage(prompt: string): Promise<void> {
const currentId = get(activeConversationStore).id;
if (!currentId) return;
// Optimistically add user message
const tempUserMsg: ConversationMessageDto = {
id: `temp-${Date.now()}`,
role: "user",
@ -138,12 +142,14 @@ export async function sendConversationMessage(prompt: string): Promise<void> {
activeConversationStore.update((s) => ({
...s,
busy: false,
// Replace temp user message + add assistant message
messages: [
...s.messages.filter((m) => m.id !== tempUserMsg.id),
result.userMessage,
result.assistantMessage,
],
}));
// Update conversation list (updated_at changes)
void loadConversations();
} catch (error) {
const errorMsg: ConversationMessageDto = {
@ -189,6 +195,7 @@ export async function removeConversation(id: string): Promise<void> {
...s,
items: s.items.filter((c) => c.id !== id),
}));
// If this was the active conversation, clear it
const active = get(activeConversationStore);
if (active.id === id) {
clearActiveConversation();
@ -210,4 +217,3 @@ export function clearActiveConversation(): void {
error: "",
});
}
//#endregion

View File

@ -10,6 +10,8 @@ import {
type TodoListDto,
} from "$lib/backend/todos";
// TodoItem keeps a numeric `id` for local array operations (used by EditorPanel)
// plus a `backendId` (guid string) for backend persistence.
export type TodoItem = {
id: number;
text: string;
@ -22,7 +24,8 @@ export const todoListsStore = writable<TodoListMeta[]>([]);
export const todosStore = writable<Record<string, TodoItem[]>>({});
export const todosBusyStore = writable(false);
//#region ID Helpers
// ── ID helpers ───────────────────────────────────────────────────
function toStoreId(guid: string): string {
return `todos/${guid}`;
}
@ -37,9 +40,9 @@ function toBackendId(storeId: string): string | null {
export function createTodoId(): number {
return Date.now() + Math.floor(Math.random() * 1000);
}
//#endregion
//#region DTO Mapping
// ── DTO mapping ──────────────────────────────────────────────────
function dtoToMeta(dto: TodoListDto): TodoListMeta {
return {
id: toStoreId(dto.id),
@ -56,9 +59,9 @@ function dtoToItems(dto: TodoListDto): TodoItem[] {
backendId: item.id,
}));
}
//#endregion
//#region Hydration
// ── Hydration ────────────────────────────────────────────────────
export async function hydrateTodos(): Promise<void> {
todosBusyStore.set(true);
try {
@ -79,9 +82,9 @@ export async function hydrateTodos(): Promise<void> {
todosBusyStore.set(false);
}
}
//#endregion
//#region List CRUD
// ── List CRUD ────────────────────────────────────────────────────
export async function createTodoListFromLabel(
label: string,
): Promise<{ meta: TodoListMeta; items: TodoItem[] }> {
@ -110,9 +113,9 @@ export async function deleteTodoListByStoreId(
});
return true;
}
//#endregion
//#region Item CRUD
// ── Item CRUD (backend-backed) ───────────────────────────────────
export async function addTodoItemBackend(
storeId: string,
text: string,
@ -201,9 +204,9 @@ export async function removeTodoItemBackend(
}));
return true;
}
//#endregion
//#region Pure Helpers
// ── Pure helpers (used by EditorPanel for local state) ───────────
export function serializeTodoList(title: string, todos: TodoItem[]): string {
const heading = title?.trim() ? `# ${title}` : "# To-Do List";
const lines = todos.map(
@ -282,4 +285,3 @@ export function createTodoListDraft(): {
items: [],
};
}
//#endregion

View File

@ -10,6 +10,7 @@ export function escapeHtml(input: string): string {
export function parseInline(input: string): string {
let value = escapeHtml(input);
// Render tag token groups like [[work, vibe]] as visual chips in preview.
value = value.replace(/\[\[([^[\]]+)\]\]/g, (match, rawGroup: string) => {
const tags = rawGroup
.split(",")
@ -24,6 +25,7 @@ export function parseInline(input: string): string {
return `<span class="markdown-tag-list">${chips}</span>`;
});
// Render hashtag-style tags (#Work) as chips in preview.
value = value.replace(
/(^|\s)#([A-Za-z0-9][A-Za-z0-9_-]*)\b/g,
(_, leading: string, tag: string) =>