mirror of
https://github.com/willchen96/mike.git
synced 2026-07-26 23:51:08 +02:00
feat: workflow, UI and document support updates
This commit is contained in:
parent
a5fe6d6e04
commit
82dcaefc43
139 changed files with 12554 additions and 2233 deletions
|
|
@ -539,7 +539,7 @@ export async function buildWorkflowStore(
|
|||
|
||||
// Seed system workflows first.
|
||||
for (const wf of SYSTEM_ASSISTANT_WORKFLOWS) {
|
||||
store.set(wf.id, { title: wf.title, prompt_md: wf.prompt_md });
|
||||
store.set(wf.id, { title: wf.title, skill_md: wf.skill_md });
|
||||
}
|
||||
|
||||
// Then overlay user-owned assistant workflows.
|
||||
|
|
@ -550,7 +550,7 @@ export async function buildWorkflowStore(
|
|||
.eq("type", "assistant");
|
||||
for (const wf of workflows ?? []) {
|
||||
if (wf.prompt_md) {
|
||||
store.set(wf.id, { title: wf.title, prompt_md: wf.prompt_md });
|
||||
store.set(wf.id, { title: wf.title, skill_md: wf.prompt_md });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -573,7 +573,7 @@ export async function buildWorkflowStore(
|
|||
if (wf.prompt_md) {
|
||||
store.set(wf.id, {
|
||||
title: wf.title,
|
||||
prompt_md: wf.prompt_md,
|
||||
skill_md: wf.prompt_md,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ Citation rules:
|
|||
- Keep quotes short, ideally 25 words or fewer, and tightly matched to the claim.
|
||||
- "page" means the sequential [Page N] marker in the provided text, not printed page numbers inside the document. Non-spreadsheet unpaginated files may have no [Page N] markers; omit "page" (or use 1) when none is present.
|
||||
- For spreadsheet sources (content shown as "## Sheet: <name>" markdown tables with a "Row" column and column-letter headers), cite by cell instead of page: set "sheet" to the sheet name and "cell" to the A1 address or range you are quoting (e.g. "B7" or "B7:C9", combining the column-letter header with the "Row" number). Put the plain cell value in "quote" with no "Row"/column-letter labels or "|" separators. Omit "page" for spreadsheet citations.
|
||||
- A cell tagged "⟨merged A1:C1⟩" spans that whole range: its value belongs to the anchor cell and the other covered cells are shown blank. When citing anything in a merged range, set "cell" to the full range from the tag (e.g. "A1:C1"), not a covered cell like "B1". Do not include the "⟨merged ...⟩" tag text in "quote".
|
||||
- For a continuous quote crossing two pages, set "page" to "N-M" and include [[PAGE_BREAK]] at the page break. Otherwise, use separate quote objects.
|
||||
- For legacy compatibility, you may also include top-level "page" and "quote" matching the first quote.
|
||||
- Omit the <CITATIONS> block when there are no citations.
|
||||
|
|
|
|||
|
|
@ -777,7 +777,7 @@ export async function runToolCalls(
|
|||
toolResults.push({
|
||||
role: "tool",
|
||||
tool_call_id: tc.id,
|
||||
content: wf ? wf.prompt_md : `Workflow '${wfId}' not found.`,
|
||||
content: wf ? wf.skill_md : `Workflow '${wfId}' not found.`,
|
||||
});
|
||||
} else if (tc.function.name === "read_table_cells" && tabularStore) {
|
||||
const colIndices = args.col_indices as number[] | undefined;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export type DocStore = Map<
|
|||
{ storage_path: string; file_type: string; filename: string }
|
||||
>;
|
||||
|
||||
export type WorkflowStore = Map<string, { title: string; prompt_md: string }>;
|
||||
export type WorkflowStore = Map<string, { title: string; skill_md: string }>;
|
||||
|
||||
export type DocIndex = Record<
|
||||
string,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@ function renderSheet(sheetName: string, ws: XLSX.WorkSheet): string | null {
|
|||
if (!ref) return null;
|
||||
const range = XLSX.utils.decode_range(ref);
|
||||
|
||||
// Map each merged range's top-left (anchor) address to its encoded range so we
|
||||
// can tag the anchor inline (e.g. `Amount ⟨merged B2:C2⟩`). The covered cells
|
||||
// stay blank, so the model never reads a covered address (e.g. B1 inside
|
||||
// A1:C1) as its own value; the tag tells it the anchor spans that range, and
|
||||
// to cite the whole range for anything in it.
|
||||
const mergeAnchors = new Map<string, string>();
|
||||
for (const m of ws["!merges"] ?? []) {
|
||||
mergeAnchors.set(XLSX.utils.encode_cell(m.s), XLSX.utils.encode_range(m));
|
||||
}
|
||||
|
||||
// Build a trimmed grid: capture formatted text for every cell in the used
|
||||
// range, then drop trailing empty columns and fully empty rows so we don't
|
||||
// emit oceans of blank cells.
|
||||
|
|
@ -44,7 +54,13 @@ function renderSheet(sheetName: string, ws: XLSX.WorkSheet): string | null {
|
|||
let rowHasContent = false;
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
const addr = XLSX.utils.encode_cell({ r, c });
|
||||
const text = sanitizeCellText(cellDisplayText(ws[addr]));
|
||||
let text = sanitizeCellText(cellDisplayText(ws[addr]));
|
||||
const mergeRange = mergeAnchors.get(addr);
|
||||
if (mergeRange) {
|
||||
text = text
|
||||
? `${text} ⟨merged ${mergeRange}⟩`
|
||||
: `⟨merged ${mergeRange}⟩`;
|
||||
}
|
||||
cells[c - range.s.c] = text;
|
||||
if (text) {
|
||||
rowHasContent = true;
|
||||
|
|
@ -72,13 +88,6 @@ function renderSheet(sheetName: string, ws: XLSX.WorkSheet): string | null {
|
|||
|
||||
const lines = [`## Sheet: ${sheetName}`, "", headerRow, separator, ...bodyRows];
|
||||
|
||||
// Note merged ranges once so the model understands spanned headers/labels.
|
||||
const merges = ws["!merges"];
|
||||
if (merges && merges.length > 0) {
|
||||
const encoded = merges.map((m) => XLSX.utils.encode_range(m));
|
||||
lines.push("", `Merged ranges: ${encoded.join(", ")}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
113
backend/src/lib/userLookup.ts
Normal file
113
backend/src/lib/userLookup.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
type Db = SupabaseClient<any, "public", any>;
|
||||
|
||||
export type ProfileUserInfo = {
|
||||
id: string;
|
||||
email: string;
|
||||
display_name: string | null;
|
||||
};
|
||||
|
||||
export function normalizeEmail(value: unknown) {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
export function normalizeDisplayName(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
export async function loadProfileUsersByEmail(db: Db) {
|
||||
const { data, error } = await db
|
||||
.from("user_profiles")
|
||||
.select("user_id, email, display_name")
|
||||
.not("email", "is", null);
|
||||
if (error) throw error;
|
||||
|
||||
const userByEmail = new Map<string, ProfileUserInfo>();
|
||||
const userById = new Map<string, ProfileUserInfo>();
|
||||
for (const row of data ?? []) {
|
||||
const email = normalizeEmail(row.email);
|
||||
if (!email) continue;
|
||||
const info = {
|
||||
id: row.user_id as string,
|
||||
email,
|
||||
display_name: normalizeDisplayName(row.display_name),
|
||||
};
|
||||
userByEmail.set(email, info);
|
||||
userById.set(info.id, info);
|
||||
}
|
||||
|
||||
return { userByEmail, userById };
|
||||
}
|
||||
|
||||
export async function findProfileUserByEmail(db: Db, email: string) {
|
||||
const normalized = normalizeEmail(email);
|
||||
if (!normalized) return null;
|
||||
|
||||
const { data, error } = await db
|
||||
.from("user_profiles")
|
||||
.select("user_id, email, display_name")
|
||||
.eq("email", normalized)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
if (!data) return null;
|
||||
|
||||
return {
|
||||
id: data.user_id as string,
|
||||
email: normalized,
|
||||
display_name: normalizeDisplayName(data.display_name),
|
||||
};
|
||||
}
|
||||
|
||||
export async function findMissingUserEmails(db: Db, emails: string[]) {
|
||||
const normalizedEmails = [...new Set(emails.map(normalizeEmail).filter(Boolean))];
|
||||
if (normalizedEmails.length === 0) return [];
|
||||
|
||||
const { data, error } = await db
|
||||
.from("user_profiles")
|
||||
.select("email")
|
||||
.in("email", normalizedEmails);
|
||||
if (error) throw error;
|
||||
|
||||
const found = new Set(
|
||||
(data ?? [])
|
||||
.map((row) => normalizeEmail(row.email))
|
||||
.filter(Boolean),
|
||||
);
|
||||
return normalizedEmails.filter((email) => !found.has(email));
|
||||
}
|
||||
|
||||
export async function syncProfileEmail(
|
||||
db: Db,
|
||||
userId: string,
|
||||
email: string | null | undefined,
|
||||
) {
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
if (!userId || !normalizedEmail) return null;
|
||||
|
||||
const { data: existing, error: loadError } = await db
|
||||
.from("user_profiles")
|
||||
.select("email")
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
if (loadError) return loadError;
|
||||
|
||||
if (!existing) {
|
||||
const { error } = await db.from("user_profiles").insert({
|
||||
user_id: userId,
|
||||
email: normalizedEmail,
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
if (normalizeEmail(existing.email) === normalizedEmail) return null;
|
||||
|
||||
const { error } = await db
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
email: normalizedEmail,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", userId);
|
||||
return error;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue