Skip to content

Commit 8f148c5

Browse files
actus7claude
andauthored
fix(db): rotas de canvas sem transacao + feat(chat): bastidores da resposta (#235)
* fix(db): rotas de canvas/artefato sem $transaction nem nested writes O adapter PrismaNeonHttp nao suporta transacoes nem writes aninhados ($transaction interno), quebrando com "Transactions are not supported in HTTP mode" (HTTP 500) na criacao/versao/pin de canvas em producao e local. - POST /conversations/:id/canvases: canvas + versao 1 sequenciais com compensacao (delete do canvas se a versao falhar) - PATCH /canvas/:id e restore: versao + update sequenciais com compensacao (remove a versao orfa se o update falhar) - POST /canvas/:id/pin: artefato + versao 1 sequenciais com compensacao - POST /projects/:id/artifacts/:id/refresh: versao + update sequenciais - Resposta serializa versoes via findMany explicito (sem include) - Testes unitarios atualizados (12/12); validado contra adapter Neon HTTP real com banco Neon (todos os shapes pos-fix funcionam) * feat(chat): bastidores da resposta (roteamento, fallback, TTFT) + nota no feedback Painel "Bastidores" por mensagem do assistente: provider/modelo efetivo, tier e motivo do roteamento automático, tentativas de fallback entre provedores, TTFT e custo — visível na linha de timestamp e num dialogo detalhado (Dialog/Table reaproveitados de components/ui). Feedback: thumbs-down agora aceita uma nota curta opcional (MessageReaction.note). Corrige dois bugs que impediam os bastidores de aparecer: - O id gerado no cliente para a mensagem do assistente nunca era usado como Message.id persistido (o servidor sempre gerava um cuid novo), entao o UsageLog nunca encontrava a mensagem certa via join. - UsageLog.messageId era @unique, mas o roteamento automatico pode tentar varios provedores (fallback) para a mesma mensagem, cada um gravando sua propria linha - a constraint descartava silenciosamente a tentativa bem-sucedida quando uma tentativa anterior ja tinha ocupado o messageId. Agora messageId nao e mais unico e GET /conversations/:id/messages agrupa todas as tentativas por messageId, escolhendo a bem-sucedida como resumo e as demais como "attempts" (fallback cross-provider). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(chat): estima tokens/s quando o provider nao reporta uso real Providers sem chave de API (Duck.ai, Pollinations, Quillbot etc.) nao expoem contagem de tokens no upstream - o roteamento automatico cai neles com frequencia, entao tokens/s ficava sempre vazio nesses casos. Quando outputTokens vem null do UsageLog, estima a partir do tamanho da resposta (~4 caracteres por token) tanto na linha inline quanto no dialogo de bastidores, sempre marcado com "~" para nao parecer um valor exato. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(chat): bastidores aparece so na primeira mensagem apos carregar a pagina POST /conversations/:id/messages nunca consultava UsageLog - so o GET seguinte fazia isso. Na pratica isso significava que so as mensagens ja presentes no ultimo carregamento da pagina mostravam bastidores; qualquer mensagem enviada depois, na mesma sessao ao vivo, ficava sem os dados ate recarregar/trocar de conversa. persistMessages agora roda a mesma consulta/agregacao de UsageLog usada no GET (reaproveitando buildBackstageByMessageId), entao a resposta do POST ja vem com os bastidores da mensagem recem-criada. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(chat): propaga bastidores do POST + feat(canvas): alternar preview/codigo O servidor ja devolvia `backstage` na resposta do POST de mensagens, mas o cliente descartava: ao reconciliar a mensagem otimista com a persistida ele copiava apenas `content` e `id`. Resultado: nenhuma mensagem enviada ao vivo mostrava TTFT/tokens-por-segundo ate recarregar a pagina. Canvas: html, react e mermaid so tinham o resultado renderizado, sem forma de inspecionar a fonte. Novo botao alterna entre preview e codigo-fonte (CodeMirror somente-leitura, com a linguagem certa por tipo). Nao aparece para canvas do tipo "code", que ja e o proprio fonte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: reforça canvas, chat e preparação para produção --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 389f0d8 commit 8f148c5

41 files changed

Lines changed: 6610 additions & 3333 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ dist
1313
build
1414
coverage
1515

16-
# Environment files
17-
.env
18-
.env.local
19-
.env.*.local
20-
.env.development.local
21-
.env.test.local
22-
.env.production.local
16+
# Environment files and local registry credentials
17+
.env*
18+
.npmrc
19+
.yarnrc.yml
20+
*.pem
21+
*.key
22+
*.p12
23+
*.pfx
2324

2425
# Git
2526
.git
@@ -40,6 +41,7 @@ Thumbs.db
4041
# Testing
4142
coverage
4243
.nyc_output
44+
.playwright-cli
4345
*.lcov
4446

4547
# Logs
@@ -81,3 +83,4 @@ temp
8183
# Monorepo
8284
apps
8385
packages
86+
manifest-main

.github/workflows/release.yml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,11 @@ jobs:
3131

3232
- name: Build
3333
run: pnpm build
34-
env:
35-
DATABASE_URL: postgresql://dummy:dummy@localhost:5432/dummy
36-
DIRECT_URL: postgresql://dummy:dummy@localhost:5432/dummy
37-
NEON_AUTH_BASE_URL: https://dummy.neon.tech
38-
NEON_AUTH_COOKIE_SECRET: dummy-secret-32-characters-long
39-
ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
4034

4135
- name: Generate Release Notes
4236
id: release_notes
4337
run: |
44-
VERSION=${GITHUB_REF#refs/tags/}
38+
VERSION=${GITHUB_REF_NAME#v}
4539
echo "version=$VERSION" >> $GITHUB_OUTPUT
4640
4741
# Extrair notas do CHANGELOG.md
@@ -86,7 +80,7 @@ jobs:
8680
type=raw,value=latest
8781
8882
- name: Build and push
89-
uses: docker/build-push-action@v5
83+
uses: docker/build-push-action@v6
9084
with:
9185
context: .
9286
push: true

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
77

88
## [Unreleased]
99

10+
### Adicionado
11+
- Projetos, canvases versionados e artefatos persistidos, com compartilhamento, histórico de versões e respectivas migrações de banco.
12+
- Bastidores das respostas de chat com roteamento, fallback, TTFT, estimativa de tokens por segundo e notas em reações.
13+
14+
### Segurança
15+
- Dependências transitivas corrigidas para eliminar os advisories conhecidos dos grafos completo e de produção.
16+
1017
### Planejado
1118
- Suporte a mais provedores (Perplexity, Together AI)
1219
- Sistema de plugins

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,13 @@ GitHub Actions runs lint, typecheck, test, and build in parallel on push/PR to m
7272
- 2-space indentation, single quotes, semicolons (Prettier configured in `.prettierrc`)
7373
- Prefer Server Components; use `"use client"` only when needed
7474
- Tests colocated with source files (e.g., `lib/chat-stream.test.ts`)
75+
76+
<!-- BEGIN:nextjs-agent-rules -->
77+
78+
# This is NOT the Next.js you know
79+
80+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
81+
82+
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
83+
84+
<!-- END:nextjs-agent-rules -->

Dockerfile

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
FROM node:22-bookworm-slim AS base
2+
3+
ENV NEXT_TELEMETRY_DISABLED=1 \
4+
PUPPETEER_SKIP_DOWNLOAD=true
5+
6+
WORKDIR /app
7+
8+
RUN corepack enable \
9+
&& corepack prepare pnpm@10.33.0 --activate
10+
11+
12+
FROM base AS dependencies
13+
14+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
15+
RUN pnpm install --frozen-lockfile --ignore-scripts
16+
17+
18+
FROM base AS production-dependencies
19+
20+
ENV NODE_ENV=production
21+
22+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
23+
RUN pnpm install --prod --frozen-lockfile --ignore-scripts
24+
25+
26+
FROM base AS builder
27+
28+
ENV NODE_ENV=production
29+
30+
COPY --from=dependencies /app/node_modules ./node_modules
31+
COPY . .
32+
33+
# Prisma 7 writes the client to generated/prisma. Generation does not require
34+
# a live database; prisma.config.ts supplies a non-secret fallback URL here.
35+
RUN pnpm prisma:generate \
36+
&& pnpm build
37+
38+
39+
FROM node:22-bookworm-slim AS runner
40+
41+
ENV NODE_ENV=production \
42+
NEXT_TELEMETRY_DISABLED=1 \
43+
PUPPETEER_SKIP_DOWNLOAD=true \
44+
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
45+
HOSTNAME=0.0.0.0 \
46+
PORT=3000
47+
48+
WORKDIR /app
49+
50+
RUN apt-get update \
51+
&& apt-get install --no-install-recommends -y ca-certificates chromium \
52+
&& rm -rf /var/lib/apt/lists/* \
53+
&& mkdir -p /app/logs \
54+
&& chown node:node /app/logs
55+
56+
COPY --from=production-dependencies --chown=node:node /app/node_modules ./node_modules
57+
COPY --from=builder --chown=node:node /app/.next ./.next
58+
COPY --from=builder --chown=node:node /app/generated ./generated
59+
COPY --from=builder --chown=node:node /app/public ./public
60+
COPY --from=builder --chown=node:node /app/next.config.ts ./next.config.ts
61+
COPY --from=builder --chown=node:node /app/package.json ./package.json
62+
63+
USER node
64+
65+
EXPOSE 3000
66+
67+
CMD ["node_modules/.bin/next", "start"]

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<a href="https://github.com/actus7/modelhub/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/actus7/modelhub/actions/workflows/ci.yml/badge.svg" /></a>
1212
<a href="LICENSE"><img alt="Licença MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
1313
<a href="https://nodejs.org"><img alt="Node.js >= 22" src="https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen" /></a>
14-
<a href="https://nextjs.org/"><img alt="Next.js 16.2" src="https://img.shields.io/badge/Next.js-16.2-black" /></a>
14+
<a href="https://nextjs.org/"><img alt="Next.js 16.3" src="https://img.shields.io/badge/Next.js-16.3-black" /></a>
1515
<a href="https://www.typescriptlang.org/"><img alt="TypeScript 5" src="https://img.shields.io/badge/TypeScript-5.x-blue" /></a>
1616
<a href="https://hono.dev/"><img alt="Hono 4" src="https://img.shields.io/badge/Hono-4.x-E36002?logo=hono&logoColor=white" /></a>
1717
<a href="https://www.prisma.io/"><img alt="Prisma 7" src="https://img.shields.io/badge/Prisma-7.x-2D3748?logo=prisma&logoColor=white" /></a>
@@ -56,6 +56,10 @@ Em vez de cada aplicação integrar vários provedores separadamente, o ModelHub
5656
<td><strong>Roteamento inteligente</strong><br />Tiers por complexidade, overrides por tarefa e fallbacks automáticos.</td>
5757
<td><strong>Anexos no chat</strong><br />Suporte a imagens, PDFs e documentos.</td>
5858
</tr>
59+
<tr>
60+
<td><strong>Canvas versionado</strong><br />Edite, visualize, restaure e compartilhe conteúdo Markdown, código, HTML, React e Mermaid.</td>
61+
<td><strong>Projetos</strong><br />Agrupe conversas, instruções, arquivos de conhecimento e artefatos reutilizáveis.</td>
62+
</tr>
5963
<tr>
6064
<td><strong>Catálogo dinâmico</strong><br />Modelos locais e busca remota quando o provider suporta.</td>
6165
<td><strong>Pronto para produção</strong><br />Rate limit, cooldown, headers de segurança, CI e deploy na Vercel.</td>
@@ -200,6 +204,7 @@ O campo `model` segue o formato `provider/model`, por exemplo:
200204
| Rota | Descrição |
201205
|---|---|
202206
| `/chat` | Conversa com provedores configurados |
207+
| `/projects` | Projetos, arquivos de conhecimento e artefatos de canvas |
203208
| `/setup` | Integrações e credenciais por provider |
204209
| `/dashboard` | API keys, uso, custos, logs e routing |
205210
| `/account` | Informações da conta |
@@ -240,7 +245,7 @@ A aplicação usa duas camadas:
240245

241246
O banco é PostgreSQL via Neon, acessado com Prisma 7 e `@prisma/adapter-neon`.
242247

243-
Modelos importantes: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `UsageLog`, `UserMemory` e `UserSettings`.
248+
Modelos importantes: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `Project`, `ProjectFile`, `ProjectArtifact`, `Canvas`, `UsageLog`, `UserMemory` e `UserSettings`.
244249

245250
Para mudanças de schema:
246251

README_EN.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<a href="https://github.com/actus7/modelhub/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/actus7/modelhub/actions/workflows/ci.yml/badge.svg" /></a>
1212
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
1313
<a href="https://nodejs.org"><img alt="Node.js >= 22" src="https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen" /></a>
14-
<a href="https://nextjs.org/"><img alt="Next.js 16.2" src="https://img.shields.io/badge/Next.js-16.2-black" /></a>
14+
<a href="https://nextjs.org/"><img alt="Next.js 16.3" src="https://img.shields.io/badge/Next.js-16.3-black" /></a>
1515
<a href="https://www.typescriptlang.org/"><img alt="TypeScript 5" src="https://img.shields.io/badge/TypeScript-5.x-blue" /></a>
1616
<a href="https://hono.dev/"><img alt="Hono 4" src="https://img.shields.io/badge/Hono-4.x-E36002?logo=hono&logoColor=white" /></a>
1717
<a href="https://www.prisma.io/"><img alt="Prisma 7" src="https://img.shields.io/badge/Prisma-7.x-2D3748?logo=prisma&logoColor=white" /></a>
@@ -56,6 +56,10 @@ Instead of each application integrating multiple providers separately, ModelHub
5656
<td><strong>Smart routing</strong><br />Tiers by complexity, per-task overrides, and automatic fallbacks.</td>
5757
<td><strong>Chat attachments</strong><br />Support for images, PDFs, and documents.</td>
5858
</tr>
59+
<tr>
60+
<td><strong>Versioned canvas</strong><br />Edit, preview, restore, and share Markdown, code, HTML, React, and Mermaid content.</td>
61+
<td><strong>Projects</strong><br />Group conversations, instructions, knowledge files, and reusable artifacts.</td>
62+
</tr>
5963
<tr>
6064
<td><strong>Dynamic catalog</strong><br />Local models and remote search when the provider supports it.</td>
6165
<td><strong>Production-ready</strong><br />Rate limiting, cooldown, security headers, CI, and Vercel deploy.</td>
@@ -200,6 +204,7 @@ The `model` field follows the `provider/model` format, for example:
200204
| Route | Description |
201205
|---|---|
202206
| `/chat` | Chat with configured providers |
207+
| `/projects` | Projects, knowledge files, and canvas artifacts |
203208
| `/setup` | Integrations and credentials per provider |
204209
| `/dashboard` | API keys, usage, costs, logs, and routing |
205210
| `/account` | Account information |
@@ -240,7 +245,7 @@ The application uses two layers:
240245

241246
The database is PostgreSQL via Neon, accessed with Prisma 7 and `@prisma/adapter-neon`.
242247

243-
Key models: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `UsageLog`, `UserMemory`, and `UserSettings`.
248+
Key models: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `Project`, `ProjectFile`, `ProjectArtifact`, `Canvas`, `UsageLog`, `UserMemory`, and `UserSettings`.
244249

245250
For schema changes:
246251

app/(app)/projects/[id]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ProjectDetailPage } from "@/components/projects/project-detail-page";
22

3-
export default function ProjectRoutePage({
3+
export default async function ProjectRoutePage({
44
params,
55
}: {
66
params: Promise<{ id: string }>;
77
}) {
8-
return <ProjectDetailPage projectIdPromise={params} />;
8+
const { id } = await params;
9+
return <ProjectDetailPage projectId={id} />;
910
}

app/accent-provider.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { createContext, useCallback, useContext, useEffect, useState } from "react";
3+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
44

55
import { apiJson } from "@/lib/api";
66
import { isValidAccentColor, type AccentColorId } from "@/lib/accent-colors";
@@ -30,20 +30,37 @@ function applyAccentToDocument(accent: AccentColorId | null) {
3030
*/
3131
export function AccentProvider({ children }: { children: React.ReactNode }) {
3232
const [accent, setAccentState] = useState<AccentColorId | null>(null);
33+
const localChangeVersionRef = useRef(0);
3334

3435
useEffect(() => {
3536
let cancelled = false;
3637

3738
async function loadAccent() {
39+
const loadVersion = localChangeVersionRef.current;
40+
const locallyApplied = document.documentElement.getAttribute("data-accent");
41+
if (isValidAccentColor(locallyApplied)) {
42+
setAccentState(locallyApplied);
43+
}
44+
3845
try {
3946
const data = await apiJson<{ settings?: { accentColor?: string | null } }>(
4047
"/user/settings",
4148
);
42-
if (cancelled) return;
43-
const stored = data.settings?.accentColor;
44-
if (isValidAccentColor(stored)) {
45-
setAccentState(stored);
46-
applyAccentToDocument(stored);
49+
if (cancelled || loadVersion !== localChangeVersionRef.current) return;
50+
if (data.settings && "accentColor" in data.settings) {
51+
const stored = data.settings.accentColor;
52+
const next = isValidAccentColor(stored) ? stored : "default";
53+
setAccentState(next);
54+
applyAccentToDocument(next);
55+
try {
56+
if (next === "default") {
57+
window.localStorage.removeItem(ACCENT_STORAGE_KEY);
58+
} else {
59+
window.localStorage.setItem(ACCENT_STORAGE_KEY, next);
60+
}
61+
} catch {
62+
// localStorage bloqueado: o valor do servidor ainda vale nesta aba.
63+
}
4764
}
4865
} catch {
4966
// Sem sessão ou offline: mantém o que o script inline aplicou.
@@ -57,6 +74,7 @@ export function AccentProvider({ children }: { children: React.ReactNode }) {
5774
}, []);
5875

5976
const setAccent = useCallback((next: AccentColorId | null) => {
77+
localChangeVersionRef.current += 1;
6078
setAccentState(next);
6179
applyAccentToDocument(next);
6280
try {

app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata, Viewport } from "next";
22
import { Inter, JetBrains_Mono, Source_Serif_4 } from "next/font/google";
3+
import Script from "next/script";
34
import { Analytics } from "@vercel/analytics/next";
45

56
import { Providers } from "./providers";
@@ -55,7 +56,9 @@ export default function RootLayout({
5556
>
5657
<head>
5758
{/* Aplica o accent salvo antes da hidratação para evitar flash (issue #177). */}
58-
<script
59+
<Script
60+
id="modelhub-accent"
61+
strategy="beforeInteractive"
5962
dangerouslySetInnerHTML={{
6063
__html: `(function(){try{var a=localStorage.getItem("modelhub-accent");var v=["blue","violet","emerald","orange","rose","teal"];if(a&&v.indexOf(a)!==-1){document.documentElement.setAttribute("data-accent",a)}}catch(e){}})();`,
6164
}}

0 commit comments

Comments
 (0)