(chore) Introduced regions

This commit is contained in:
Jacob Schmidt 2026-03-02 19:56:14 -06:00
parent ae70fbdae9
commit b0cd38e54d
7 changed files with 41 additions and 50 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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