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

View File

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

View File

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

View File

@ -10,8 +10,7 @@ import {
type ConversationMessageDto,
} from "$lib/backend/conversations";
// ── Store shapes ────────────────────────────────────────────────
//#region Store Shapes
type ConversationsState = {
items: ConversationDto[];
busy: boolean;
@ -25,9 +24,9 @@ type ActiveConversationState = {
busy: boolean;
error: string;
};
//#endregion
// ── Stores ──────────────────────────────────────────────────────
//#region Stores
export const conversationsStore = writable<ConversationsState>({
items: [],
busy: false,
@ -41,9 +40,9 @@ export const activeConversationStore = writable<ActiveConversationState>({
busy: false,
error: "",
});
//#endregion
// ── Actions ─────────────────────────────────────────────────────
//#region Actions
export async function loadConversations(): Promise<void> {
conversationsStore.update((s) => ({ ...s, busy: true, error: "" }));
try {
@ -67,7 +66,6 @@ export async function createNewConversation(
...s,
items: [conv, ...s.items],
}));
// Auto-open the new conversation
activeConversationStore.set({
id: conv.id,
title: conv.title,
@ -113,7 +111,6 @@ 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;
@ -122,7 +119,6 @@ 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",
@ -142,14 +138,12 @@ 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 = {
@ -195,7 +189,6 @@ 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();
@ -217,3 +210,4 @@ export function clearActiveConversation(): void {
error: "",
});
}
//#endregion

View File

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

View File

@ -10,7 +10,6 @@ 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(",")
@ -25,7 +24,6 @@ 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) =>