From 20ccf1e9c57adfb02c4469c7569dbd9aef0deaa5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:55:15 +0000 Subject: [PATCH 01/13] Work in progress --- src/routeTree.gen.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 0fe84b5..0bb5aca 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -670,3 +670,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} From c0d18506f83ac9dcc3b812233276dad6afad07d4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:56:14 +0000 Subject: [PATCH 02/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/integrations/supabase/types.ts | 3 ++ ...1_c21add0c-d602-4e84-8f00-0f00d15426b5.sql | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 supabase/migrations/20260528215611_c21add0c-d602-4e84-8f00-0f00d15426b5.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 9e43fae..1061afc 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -694,6 +694,7 @@ export type Database = { current_version_id: string | null description: string id: string + is_default: boolean name: string status: string trigger_keywords: string @@ -706,6 +707,7 @@ export type Database = { current_version_id?: string | null description: string id?: string + is_default?: boolean name: string status?: string trigger_keywords: string @@ -718,6 +720,7 @@ export type Database = { current_version_id?: string | null description?: string id?: string + is_default?: boolean name?: string status?: string trigger_keywords?: string diff --git a/supabase/migrations/20260528215611_c21add0c-d602-4e84-8f00-0f00d15426b5.sql b/supabase/migrations/20260528215611_c21add0c-d602-4e84-8f00-0f00d15426b5.sql new file mode 100644 index 0000000..87c0dd9 --- /dev/null +++ b/supabase/migrations/20260528215611_c21add0c-d602-4e84-8f00-0f00d15426b5.sql @@ -0,0 +1,46 @@ +-- Add is_default column to skills and update user_skill_limits view +-- to exclude default skills from custom skill quota +ALTER TABLE public.skills + ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false; + +-- Mark known default skills by name (where description matches the template too) +UPDATE public.skills + SET is_default = true + WHERE is_default = false + AND name IN ('Resumo diario', 'Planejamento semanal', 'Preparar reuniao') + AND description IN ( + 'Gera um resumo curto do dia com compromissos, tarefas e proximas prioridades.', + 'Ajuda o usuario a transformar objetivos da semana em prioridades e proximas acoes.', + 'Monta um briefing rapido antes de reunioes com contexto, pauta e perguntas uteis.' + ); + +-- Recreate user_skill_limits to exclude default skills from quota count +DROP VIEW IF EXISTS public.user_skill_limits; + +CREATE VIEW public.user_skill_limits AS +SELECT + p.id AS user_id, + pl.slug AS plan_slug, + CASE + WHEN pl.slug IS NULL THEN NULL::integer + WHEN pl.slug = 'basic' THEN 5 + WHEN pl.slug = 'starter' THEN 15 + WHEN pl.slug = 'professional' THEN 50 + WHEN pl.slug = 'enterprise' THEN 999999 + ELSE NULL::integer + END AS max_skills, + ( + SELECT count(*)::integer + FROM public.skills sk + WHERE sk.user_id = p.id + AND sk.status <> 'archived' + AND COALESCE(sk.is_default, false) = false + ) AS current_skills_count +FROM public.profiles p +LEFT JOIN public.subscriptions s + ON s.user_id = p.id + AND s.status = ANY (ARRAY['active'::text, 'trialing'::text]) +LEFT JOIN public.plans pl ON pl.id = s.plan_id; + +GRANT SELECT ON public.user_skill_limits TO authenticated; +GRANT SELECT ON public.user_skill_limits TO service_role; \ No newline at end of file From b687ad9b7226eda1bf98d969afeffd8579759a20 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:56:27 +0000 Subject: [PATCH 03/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routeTree.gen.ts | 9 --------- supabase/functions/_shared/default-skills.ts | 2 ++ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 0bb5aca..0fe84b5 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -670,12 +670,3 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } -} diff --git a/supabase/functions/_shared/default-skills.ts b/supabase/functions/_shared/default-skills.ts index 74ec5e9..cd30e10 100644 --- a/supabase/functions/_shared/default-skills.ts +++ b/supabase/functions/_shared/default-skills.ts @@ -247,9 +247,11 @@ export async function ensureDefaultSkillsForAgent( description: template.description, trigger_keywords: template.trigger_keywords, status: "draft", + is_default: true, }) .select("id") .single(); + const skill = skillData as { id: string } | null; if (skillErr || !skill) { From 2fe5d4a33e8d04b405e64a850b347d91a6dad0f2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:56:41 +0000 Subject: [PATCH 04/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../functions/test-skill-dry-run/index.ts | 90 +++++++++++-------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/supabase/functions/test-skill-dry-run/index.ts b/supabase/functions/test-skill-dry-run/index.ts index 648b2ff..acec10a 100644 --- a/supabase/functions/test-skill-dry-run/index.ts +++ b/supabase/functions/test-skill-dry-run/index.ts @@ -35,6 +35,9 @@ Deno.serve(async (req) => { const userId = userData.user.id; let body: { skill_version_id?: string; test_input?: string }; + try { + body = await req.json(); + let body: { skill_version_id?: string; test_input?: string; markdown_content?: string }; try { body = await req.json(); } catch { @@ -44,53 +47,62 @@ Deno.serve(async (req) => { }); } - const { skill_version_id, test_input } = body; - if (!skill_version_id || !test_input || test_input.trim().length === 0) { + const { skill_version_id, test_input, markdown_content } = body; + if (!test_input || test_input.trim().length === 0) { return new Response( - JSON.stringify({ error: "skill_version_id e test_input são obrigatórios" }), + JSON.stringify({ error: "test_input é obrigatório" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }, ); } - // Verifica ownership: skill_version -> skill -> user_id - const { data: versionRow, error: vErr } = await admin - .from("skill_versions") - .select("id, markdown_content, skills!inner(user_id)") - .eq("id", skill_version_id) - .maybeSingle(); + // Stateless mode: markdown_content direto (preview de nova skill, sem persistência) + const isStateless = + !!markdown_content && + markdown_content.trim().length > 0 && + (!skill_version_id || skill_version_id === "preview"); - if (vErr || !versionRow) { - return new Response(JSON.stringify({ error: "Versão não encontrada" }), { - status: 404, - headers: { ...corsHeaders, "Content-Type": "application/json" }, - }); - } - // @ts-expect-error supabase nested type - if (versionRow.skills.user_id !== userId) { - return new Response(JSON.stringify({ error: "Acesso negado" }), { - status: 403, - headers: { ...corsHeaders, "Content-Type": "application/json" }, - }); + let resolvedMarkdown: string | null = null; + let persistedVersionId: string | null = null; + + if (isStateless) { + if (markdown_content!.length > 50000) { + return new Response(JSON.stringify({ error: "markdown_content excede 50.000 caracteres" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + resolvedMarkdown = markdown_content!; + } else { + if (!skill_version_id) { + return new Response( + JSON.stringify({ error: "skill_version_id ou markdown_content é obrigatório" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }, + ); + } + // Verifica ownership: skill_version -> skill -> user_id + const { data: versionRow, error: vErr } = await admin + .from("skill_versions") + .select("id, markdown_content, skills!inner(user_id)") + .eq("id", skill_version_id) + .maybeSingle(); + + if (vErr || !versionRow) { + return new Response(JSON.stringify({ error: "Versão não encontrada" }), { + status: 404, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + // @ts-expect-error supabase nested type + if (versionRow.skills.user_id !== userId) { + return new Response(JSON.stringify({ error: "Acesso negado" }), { + status: 403, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + resolvedMarkdown = versionRow.markdown_content as string; + persistedVersionId = skill_version_id; } - // Cria registro running - const { data: runRow, error: runErr } = await admin - .from("skill_test_runs") - .insert({ - skill_version_id, - user_id: userId, - test_input, - status: "running", - test_type: "dry_run", - }) - .select("id") - .single(); - - if (runErr || !runRow) { - console.error("Failed to create test run:", runErr); - return new Response(JSON.stringify({ error: "Falha ao registrar teste" }), { - status: 500, - headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } From 9d9ca17ee70036c39015a06542c1420b922312dd Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:57:04 +0000 Subject: [PATCH 05/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../functions/test-skill-dry-run/index.ts | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/supabase/functions/test-skill-dry-run/index.ts b/supabase/functions/test-skill-dry-run/index.ts index acec10a..07ecc37 100644 --- a/supabase/functions/test-skill-dry-run/index.ts +++ b/supabase/functions/test-skill-dry-run/index.ts @@ -103,12 +103,38 @@ Deno.serve(async (req) => { persistedVersionId = skill_version_id; } - }); + // Cria registro running apenas no modo persistido + let runId: string | null = null; + if (persistedVersionId) { + const { data: runRow, error: runErr } = await admin + .from("skill_test_runs") + .insert({ + skill_version_id: persistedVersionId, + user_id: userId, + test_input, + status: "running", + test_type: "dry_run", + }) + .select("id") + .single(); + + if (runErr || !runRow) { + console.error("Failed to create test run:", runErr); + return new Response(JSON.stringify({ error: "Falha ao registrar teste" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + runId = runRow.id; } - const runId = runRow.id; const startedAt = Date.now(); + const updateRun = async (patch: Record) => { + if (!runId) return; + await admin.from("skill_test_runs").update(patch).eq("id", runId); + }; + try { const aiRes = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", { method: "POST", @@ -122,7 +148,7 @@ Deno.serve(async (req) => { { role: "system", content: SYSTEM_PROMPT }, { role: "user", - content: `Definição da skill:\n\`\`\`\n${versionRow.markdown_content}\n\`\`\`\n\nInput do usuário: ${test_input}`, + content: `Definição da skill:\n\`\`\`\n${resolvedMarkdown}\n\`\`\`\n\nInput do usuário: ${test_input}`, }, ], }), @@ -130,14 +156,11 @@ Deno.serve(async (req) => { if (aiRes.status === 429) { const duration = Date.now() - startedAt; - await admin - .from("skill_test_runs") - .update({ - status: "error", - error_message: "Muitas requisições. Aguarde 1 minuto.", - duration_ms: duration, - }) - .eq("id", runId); + await updateRun({ + status: "error", + error_message: "Muitas requisições. Aguarde 1 minuto.", + duration_ms: duration, + }); return new Response( JSON.stringify({ error: "Muitas requisições. Aguarde 1 minuto e tente novamente." }), { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } }, @@ -147,14 +170,11 @@ Deno.serve(async (req) => { if (!aiRes.ok) { const txt = await aiRes.text(); const duration = Date.now() - startedAt; - await admin - .from("skill_test_runs") - .update({ - status: "error", - error_message: `AI error ${aiRes.status}: ${txt.slice(0, 200)}`, - duration_ms: duration, - }) - .eq("id", runId); + await updateRun({ + status: "error", + error_message: `AI error ${aiRes.status}: ${txt.slice(0, 200)}`, + duration_ms: duration, + }); return new Response(JSON.stringify({ error: "Falha ao executar teste." }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, @@ -165,10 +185,7 @@ Deno.serve(async (req) => { const test_output: string = (data?.choices?.[0]?.message?.content ?? "").trim(); const duration = Date.now() - startedAt; - await admin - .from("skill_test_runs") - .update({ status: "success", test_output, duration_ms: duration }) - .eq("id", runId); + await updateRun({ status: "success", test_output, duration_ms: duration }); return new Response( JSON.stringify({ test_output, duration_ms: duration, status: "success" }), @@ -177,13 +194,11 @@ Deno.serve(async (req) => { } catch (e) { const duration = Date.now() - startedAt; const msg = e instanceof Error ? e.message : "Erro desconhecido"; - await admin - .from("skill_test_runs") - .update({ status: "error", error_message: msg, duration_ms: duration }) - .eq("id", runId); + await updateRun({ status: "error", error_message: msg, duration_ms: duration }); return new Response(JSON.stringify({ error: "Erro ao executar teste." }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } }); + From 922a652c7ea0e5634ca8dc33f2befd73e5c0b3ac Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:57:15 +0000 Subject: [PATCH 06/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/skills/SkillTestPanel.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/components/mika/skills/SkillTestPanel.tsx b/src/components/mika/skills/SkillTestPanel.tsx index 98d9ddb..2039430 100644 --- a/src/components/mika/skills/SkillTestPanel.tsx +++ b/src/components/mika/skills/SkillTestPanel.tsx @@ -78,30 +78,28 @@ export function SkillTestPanel({ mutationFn: async (): Promise => { if (stateless) { // Modo preview sem persistência: chama AI direto via edge function - const start = Date.now(); - const { data, error } = await supabase.functions.invoke("test-skill-dry-run", { - body: { skill_version_id: skillVersionId, test_input: input }, - }); - if (error) { - return { - status: "error", - duration_ms: Date.now() - start, - error_message: error.message, - }; - } - return data as TestResult; + const runTest = useMutation({ + mutationFn: async (): Promise => { + const start = Date.now(); + const requestBody: Record = { test_input: input }; + if (stateless) { + requestBody.markdown_content = stateless.markdown_content; + } else { + requestBody.skill_version_id = skillVersionId; } const { data, error } = await supabase.functions.invoke("test-skill-dry-run", { - body: { skill_version_id: skillVersionId, test_input: input }, + body: requestBody, }); - if (error) throw new Error(error.message); + if (error) { + return { + status: "error", + duration_ms: Date.now() - start, + error_message: error.message, + }; + } return data as TestResult; }, - onSuccess: (r) => { - setResult(r); - qc.invalidateQueries({ queryKey: ["skill-test-runs", skillVersionId] }); - }, - onError: (e: unknown) => { + setResult({ status: "error", duration_ms: 0, From f3308f362a169ab8346e9f848a030423e2c491a5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:57:27 +0000 Subject: [PATCH 07/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/skills/SkillTestPanel.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/mika/skills/SkillTestPanel.tsx b/src/components/mika/skills/SkillTestPanel.tsx index 2039430..591ff2b 100644 --- a/src/components/mika/skills/SkillTestPanel.tsx +++ b/src/components/mika/skills/SkillTestPanel.tsx @@ -99,7 +99,11 @@ export function SkillTestPanel({ } return data as TestResult; }, - + onSuccess: (r) => { + setResult(r); + qc.invalidateQueries({ queryKey: ["skill-test-runs", skillVersionId] }); + }, + onError: (e: unknown) => { setResult({ status: "error", duration_ms: 0, @@ -108,6 +112,7 @@ export function SkillTestPanel({ }, }); + return ( From f1a613b4fecc4620c5cd08864eb4c46e12f747f7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:57:39 +0000 Subject: [PATCH 08/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/skills/SkillTestPanel.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/mika/skills/SkillTestPanel.tsx b/src/components/mika/skills/SkillTestPanel.tsx index 591ff2b..a2586f2 100644 --- a/src/components/mika/skills/SkillTestPanel.tsx +++ b/src/components/mika/skills/SkillTestPanel.tsx @@ -74,10 +74,6 @@ export function SkillTestPanel({ }, }); - const runTest = useMutation({ - mutationFn: async (): Promise => { - if (stateless) { - // Modo preview sem persistência: chama AI direto via edge function const runTest = useMutation({ mutationFn: async (): Promise => { const start = Date.now(); From eaede6fd152e519cef1adafca3ef31806c852ac0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:57:47 +0000 Subject: [PATCH 09/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/painel.skills.preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/painel.skills.preview.tsx b/src/routes/painel.skills.preview.tsx index 0c35c2b..b64d2ad 100644 --- a/src/routes/painel.skills.preview.tsx +++ b/src/routes/painel.skills.preview.tsx @@ -228,7 +228,7 @@ function SkillPreviewPage() { open={testOpen} onOpenChange={setTestOpen} skillName={(formInputs as Record).name || "Skill"} - skillVersionId="preview" + skillVersionId="" triggerKeywords={(formInputs as Record).trigger_keywords} stateless={{ markdown_content: markdown }} /> From 284e6978013288fd7c35dcae6e2ad9fad553dafa Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:57:59 +0000 Subject: [PATCH 10/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/skills/SkillCard.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/mika/skills/SkillCard.tsx b/src/components/mika/skills/SkillCard.tsx index 687071f..ad44797 100644 --- a/src/components/mika/skills/SkillCard.tsx +++ b/src/components/mika/skills/SkillCard.tsx @@ -191,11 +191,19 @@ export function SkillCard({ skill }: { skill: Skill }) {
- +
+ + {(skill as Skill & { is_default?: boolean }).is_default && ( + + Padrão + + )} +
{formatDistanceToNow(new Date(skill.updated_at), { addSuffix: true, locale: ptBR })}
+ From 53ff827d3d8ba249ea7d29d8f58963d3bf2e97eb Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:58:07 +0000 Subject: [PATCH 11/13] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/painel.skills.index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/painel.skills.index.tsx b/src/routes/painel.skills.index.tsx index 5c5f32b..bef09f0 100644 --- a/src/routes/painel.skills.index.tsx +++ b/src/routes/painel.skills.index.tsx @@ -67,10 +67,11 @@ function SkillsPage() {
{limits.data && limits.data.max_skills != null && ( - {limits.data.current_skills_count} de {limits.data.max_skills} skills + {limits.data.current_skills_count} de {limits.data.max_skills} skills personalizadas )} +