sagadeck 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -65
- package/SKILL.md +18 -11
- package/bin/sagadeck.js +3 -10
- package/docs/ANDAMENTO.md +72 -2
- package/docs/REFERENCIA.md +2 -2
- package/package.json +2 -3
- package/src/ai/deck-ai.js +1 -1
- package/src/ai/ia-config.js +101 -0
- package/src/ai/llm.js +62 -26
- package/src/library.js +8 -4
- package/src/motion.js +2 -2
- package/src/preferences.js +3 -0
- package/src/research/research.js +6 -5
- package/src/runtime/motion.js +5 -1
- package/src/studio/ai-chat-route.js +1 -1
- package/src/studio/banner.js +18 -0
- package/src/studio/ia-routes.js +53 -0
- package/src/studio/public/ai-settings.js +76 -0
- package/src/studio/public/app.js +4 -23
- package/src/studio/public/index.html +1 -0
- package/src/studio/public/library.html +2 -1
- package/src/studio/public/library.js +5 -4
- package/src/studio/public/preferences-ui.js +3 -0
- package/src/studio/server.js +6 -18
- package/src/studio/slide-copy.js +3 -1
- package/src/ai/relay.js +0 -45
package/src/studio/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import {chatErrorResult,installErrorResponses} from './errors.js';
|
|
|
5
5
|
// sagadeck Studio · Servidor HTTP local para o editor visual PowerPoint + Chat Lateral IA
|
|
6
6
|
import { slideCopyRoutes } from "./slide-copy-routes.js";
|
|
7
7
|
import { publicScript } from "./public-files.js";
|
|
8
|
+
import { iaRoutes } from "./ia-routes.js";
|
|
8
9
|
import { codeWatch } from "./code-version.js";
|
|
9
10
|
import {meetingRoutes,meetingPreview} from '../meeting.js';
|
|
10
11
|
import { codeRoutes } from "./code-routes.js";
|
|
@@ -40,7 +41,7 @@ import { generateForStudio } from "./generate.js";
|
|
|
40
41
|
import { runCommand, logCommand } from "../ai/commands.js";
|
|
41
42
|
import { needsApproval, approvalLabel, awaitApproval, commandEnv, estimateSubmitCost } from "./command-approval.js";
|
|
42
43
|
import { demoDeck, demoAssets, demoProjectFiles } from "./demo-decks.js";
|
|
43
|
-
import { llmAvailable, llmConfig } from "../ai/llm.js";
|
|
44
|
+
import { llmAvailable, llmConfig, llmConfigured } from "../ai/llm.js";
|
|
44
45
|
import { editDeck, textToSlide, toYaml, materializeImages } from "../ai/deck-ai.js";
|
|
45
46
|
import { transformDeck, jobStatus, sourceHash } from "../ai/transform.js";
|
|
46
47
|
import { sendExport, sendHtml, lightVariant } from "./exporting.js";
|
|
@@ -892,27 +893,14 @@ export function createStudioServer(deckPath = null, opts = {}) {
|
|
|
892
893
|
return;
|
|
893
894
|
}
|
|
894
895
|
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
if (pathname === "/api/ai/setup" && req.method === "GET") {
|
|
898
|
-
let setup = null;
|
|
899
|
-
const base = llmConfig().url.replace(/\/v1$/, "");
|
|
900
|
-
if (!opts.multiuser && /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(base)) {
|
|
901
|
-
try {
|
|
902
|
-
const r = await fetch(base + "/api/console/config", { signal: AbortSignal.timeout(1500) });
|
|
903
|
-
if (r.ok) setup = base + "/";
|
|
904
|
-
} catch {}
|
|
905
|
-
}
|
|
906
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
907
|
-
res.end(JSON.stringify({ url: setup }));
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
896
|
+
// Configurar IA (provedor, chave, modelos desta máquina): src/studio/ia-routes.js
|
|
897
|
+
if (pathname.startsWith("/api/ia") && await iaRoutes({ req, res, pathname, opts, readJSON })) return;
|
|
910
898
|
|
|
911
899
|
if (pathname === "/api/ai/status" && req.method === "GET") {
|
|
912
900
|
const cfg = llmConfig();
|
|
913
901
|
const available = await llmAvailable({ force: url.searchParams.has("refresh") });
|
|
914
902
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
915
|
-
res.end(JSON.stringify({ available, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
|
|
903
|
+
res.end(JSON.stringify({ available, configured: llmConfigured(cfg), source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
|
|
916
904
|
return;
|
|
917
905
|
}
|
|
918
906
|
|
|
@@ -954,7 +942,7 @@ export function createStudioServer(deckPath = null, opts = {}) {
|
|
|
954
942
|
}
|
|
955
943
|
if (!(await llmAvailable({ force: true }))) {
|
|
956
944
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
957
|
-
res.end(JSON.stringify({ error: `Nenhum LLM respondendo em ${llmConfig().url}.
|
|
945
|
+
res.end(JSON.stringify({ error: `${llmConfigured() ? `Nenhum LLM respondendo em ${llmConfig().url}. Confira o provedor e a chave em Configurar IA.` : "A IA não está configurada: clique em Configurar IA e informe provedor e chave."}` }));
|
|
958
946
|
return;
|
|
959
947
|
}
|
|
960
948
|
// O deck novo vai para uma pasta própria na biblioteca (regra do CLAUDE.md), no tópico do deck aberto
|
package/src/studio/slide-copy.js
CHANGED
|
@@ -11,7 +11,9 @@ export async function copySlideAssets(slide, sourceFile, targetFile) {
|
|
|
11
11
|
async function file(ref,from=source){
|
|
12
12
|
const abs=path.resolve(from,ref.split(/[?#]/)[0]);const rel=path.relative(source,abs);
|
|
13
13
|
if(rel.startsWith('..')||path.isAbsolute(rel))throw Error('A mídia deve estar dentro da pasta da apresentação de origem.');
|
|
14
|
-
|
|
14
|
+
// os dois lados pelo caminho real: a pasta aberta por junção (ou nome curto do Windows, RUNNER~1) não bate com o
|
|
15
|
+
// caminho resolvido do arquivo e toda mídia parecia "fora da apresentação"
|
|
16
|
+
const real=await fs.realpath(abs);const rr=path.relative(await fs.realpath(source),real);if(rr.startsWith('..')||path.isAbsolute(rr))throw Error('Referência de mídia fora da apresentação.');
|
|
15
17
|
const out=path.join(target,prefix,rel);
|
|
16
18
|
if(!copied.has(abs)){copied.add(abs);await fs.mkdir(path.dirname(out),{recursive:true});await fs.copyFile(abs,out);
|
|
17
19
|
if(/\.(html?|css)$/i.test(abs)){const text=await fs.readFile(abs,'utf8');for(const m of text.matchAll(/(?:src|href)\s*=\s*["']([^"']+)["']|url\(\s*["']?([^)'"\s]+)["']?\s*\)/g)){const dep=m[1]||m[2];if(local(dep))await file(dep,path.dirname(abs));}}
|
package/src/ai/relay.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Studio em Node: sobe o `modelrelay serve` junto, como o sagadeck do pip já faz (python/sagadeck/llm.py).
|
|
2
|
-
// Só quando faz sentido: ninguém escutando na porta padrão, sem SAGADECK_LLM_URL e sem SAGADECK_NO_RELAY=1.
|
|
3
|
-
// A configuração (provedor, modelos, chave) continua sendo do modelrelay (~/.modelrelay/config.toml).
|
|
4
|
-
import net from "node:net";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
export const RELAY_PORT = 8765;
|
|
8
|
-
|
|
9
|
-
export function portOpen(port, host = "127.0.0.1", timeoutMs = 300) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
const s = net.connect({ port, host });
|
|
12
|
-
const done = (ok) => { s.destroy(); resolve(ok); };
|
|
13
|
-
s.setTimeout(timeoutMs, () => done(false));
|
|
14
|
-
s.once("connect", () => done(true));
|
|
15
|
-
s.once("error", () => done(false));
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// o comando `modelrelay` (script do pip) e, se ele não estiver no PATH, o módulo pelo Python
|
|
20
|
-
const candidates = (port) => [
|
|
21
|
-
["modelrelay", ["serve", "--port", String(port)]],
|
|
22
|
-
["python", ["-m", "modelrelay.cli", "serve", "--port", String(port)]],
|
|
23
|
-
["python3", ["-m", "modelrelay.cli", "serve", "--port", String(port)]],
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
export async function startRelay({ env = process.env, port = RELAY_PORT, spawnFn = spawn, timeoutMs = 8000 } = {}) {
|
|
27
|
-
if (env.SAGADECK_LLM_URL || env.SAGADECK_NO_RELAY === "1") return null;
|
|
28
|
-
if (await portOpen(port)) return null; // já tem um rodando: o motor acha sozinho
|
|
29
|
-
for (const [cmd, args] of candidates(port)) {
|
|
30
|
-
let child;
|
|
31
|
-
try { child = spawnFn(cmd, args, { stdio: "ignore", windowsHide: true, env }); } catch { continue; }
|
|
32
|
-
const failed = await new Promise((resolve) => {
|
|
33
|
-
let over = false;
|
|
34
|
-
const finish = (v) => { if (!over) { over = true; clearInterval(poll); clearTimeout(limit); resolve(v); } };
|
|
35
|
-
child.once?.("error", () => finish(true)); // comando não existe
|
|
36
|
-
child.once?.("exit", () => finish(true)); // existe, mas sem o modelrelay ou com config inválida
|
|
37
|
-
const poll = setInterval(async () => { if (await portOpen(port)) finish(false); }, 150);
|
|
38
|
-
const limit = setTimeout(() => finish(true), timeoutMs);
|
|
39
|
-
});
|
|
40
|
-
if (failed) { try { child.kill?.(); } catch {} continue; }
|
|
41
|
-
const stop = () => { try { child.kill?.(); } catch {} };
|
|
42
|
-
return { url: `http://127.0.0.1:${port}/v1`, cmd, stop };
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|