Jacob Schmidt 192e6e3891 feat: add AI coaching, conversation persistence, and LLamaSharp integration
- Add Journal.AI project with LLamaSharp-based AI service (Phi-3 model)
- Implement coach sessions (daily check-in, evening review, weekly review)
- Add conversation CRUD with SQLCipher persistence
- AI chat with full conversation history for context-aware replies
- Frontend: CoachPanel, AI stores, conversation stores, side panel UI
- Conversation list with create, rename, and delete support
- Fix Phi-3 output quality (system prompt leaking, token cleanup, JSON filtering)
- Fix CREATEDRAFT kind override in coach sessions

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-03-01 16:07:59 -06:00

223 lines
5.5 KiB
TypeScript

import { sendCommand } from "./client";
import { pickCase } from "./normalize";
// ── Public types ────────────────────────────────────────────────
export type AiHealthDto = {
provider: string;
enabled: boolean;
healthy: boolean;
message: string;
};
export type CoachEvidenceDto = {
recordId: string | null;
text: string;
};
export type CoachPatchProposalDto = {
kind: string;
description: string | null;
content: string | null;
};
export type CoachPlanDto = {
kind: string;
title: string;
summary: string;
questions: string[];
suggestedNextActions: string[];
suggestedTags: string[];
evidence: CoachEvidenceDto[];
patchProposal: CoachPatchProposalDto | null;
};
export type CoachPreferencesDto = {
maxQuestions?: number;
maxNextActions?: number;
};
export type CoachSessionPayload = {
dateLocal?: string;
weekStartLocal?: string;
weekEndLocal?: string;
recentEntries?: string[];
recentFragments?: string[];
preferences?: CoachPreferencesDto;
};
// ── Raw (PascalCase) variants for normalization ─────────────────
type AiHealthDtoRaw = {
provider?: string;
enabled?: boolean;
healthy?: boolean;
message?: string;
Provider?: string;
Enabled?: boolean;
Healthy?: boolean;
Message?: string;
};
type CoachEvidenceDtoRaw = {
recordId?: string | null;
text?: string;
RecordId?: string | null;
Text?: string;
};
type CoachPatchProposalDtoRaw = {
kind?: string;
description?: string | null;
content?: string | null;
Kind?: string;
Description?: string | null;
Content?: string | null;
};
type CoachPlanDtoRaw = {
kind?: string;
title?: string;
summary?: string;
questions?: string[];
suggestedNextActions?: string[];
suggestedTags?: string[];
evidence?: CoachEvidenceDtoRaw[];
patchProposal?: CoachPatchProposalDtoRaw | null;
Kind?: string;
Title?: string;
Summary?: string;
Questions?: string[];
SuggestedNextActions?: string[];
SuggestedTags?: string[];
Evidence?: CoachEvidenceDtoRaw[];
PatchProposal?: CoachPatchProposalDtoRaw | null;
};
// ── Normalizers ─────────────────────────────────────────────────
function normalizeHealth(raw: AiHealthDtoRaw): AiHealthDto {
return {
provider: pickCase(raw, "provider", "Provider", ""),
enabled: pickCase(raw, "enabled", "Enabled", false),
healthy: pickCase(raw, "healthy", "Healthy", false),
message: pickCase(raw, "message", "Message", ""),
};
}
function normalizeEvidence(raw: CoachEvidenceDtoRaw): CoachEvidenceDto {
return {
recordId: pickCase(raw, "recordId", "RecordId", null as string | null),
text: pickCase(raw, "text", "Text", ""),
};
}
function normalizePatchProposal(
raw: CoachPatchProposalDtoRaw | null | undefined,
): CoachPatchProposalDto | null {
if (!raw) return null;
return {
kind: pickCase(raw, "kind", "Kind", ""),
description: pickCase(
raw,
"description",
"Description",
null as string | null,
),
content: pickCase(raw, "content", "Content", null as string | null),
};
}
function normalizeCoachPlan(raw: CoachPlanDtoRaw): CoachPlanDto {
const evidenceRaw = pickCase(
raw,
"evidence",
"Evidence",
[] as CoachEvidenceDtoRaw[],
);
const patchRaw = pickCase(
raw,
"patchProposal",
"PatchProposal",
null as CoachPatchProposalDtoRaw | null,
);
return {
kind: pickCase(raw, "kind", "Kind", ""),
title: pickCase(raw, "title", "Title", ""),
summary: pickCase(raw, "summary", "Summary", ""),
questions: pickCase(raw, "questions", "Questions", [] as string[]),
suggestedNextActions: pickCase(
raw,
"suggestedNextActions",
"SuggestedNextActions",
[] as string[],
),
suggestedTags: pickCase(
raw,
"suggestedTags",
"SuggestedTags",
[] as string[],
),
evidence: evidenceRaw.map(normalizeEvidence),
patchProposal: normalizePatchProposal(patchRaw),
};
}
// ── API functions ───────────────────────────────────────────────
export async function aiHealth(): Promise<AiHealthDto> {
const data = await sendCommand<AiHealthDtoRaw>({
action: "ai.health",
payload: {},
});
return normalizeHealth(data);
}
export async function aiChat(prompt: string): Promise<string> {
return sendCommand<string>({
action: "ai.chat",
payload: { prompt },
});
}
export async function aiSummarizeEntry(
content: string,
fileStem?: string,
): Promise<string> {
return sendCommand<string>({
action: "ai.summarize_entry",
payload: { content, fileStem },
});
}
export async function coachDaily(
payload: CoachSessionPayload = {},
): Promise<CoachPlanDto> {
const data = await sendCommand<CoachPlanDtoRaw>({
action: "ai.coach.daily",
payload,
});
return normalizeCoachPlan(data);
}
export async function coachEvening(
payload: CoachSessionPayload = {},
): Promise<CoachPlanDto> {
const data = await sendCommand<CoachPlanDtoRaw>({
action: "ai.coach.evening",
payload,
});
return normalizeCoachPlan(data);
}
export async function coachWeekly(
payload: CoachSessionPayload = {},
): Promise<CoachPlanDto> {
const data = await sendCommand<CoachPlanDtoRaw>({
action: "ai.coach.weekly",
payload,
});
return normalizeCoachPlan(data);
}