diff --git a/.dockerignore b/.dockerignore
index e9ceda6..26ebad9 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -13,13 +13,14 @@ dist
build
coverage
-# Environment files
-.env
-.env.local
-.env.*.local
-.env.development.local
-.env.test.local
-.env.production.local
+# Environment files and local registry credentials
+.env*
+.npmrc
+.yarnrc.yml
+*.pem
+*.key
+*.p12
+*.pfx
# Git
.git
@@ -40,6 +41,7 @@ Thumbs.db
# Testing
coverage
.nyc_output
+.playwright-cli
*.lcov
# Logs
@@ -81,3 +83,4 @@ temp
# Monorepo
apps
packages
+manifest-main
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2dde9bd..6b30009 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,17 +31,11 @@ jobs:
- name: Build
run: pnpm build
- env:
- DATABASE_URL: postgresql://dummy:dummy@localhost:5432/dummy
- DIRECT_URL: postgresql://dummy:dummy@localhost:5432/dummy
- NEON_AUTH_BASE_URL: https://dummy.neon.tech
- NEON_AUTH_COOKIE_SECRET: dummy-secret-32-characters-long
- ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
- name: Generate Release Notes
id: release_notes
run: |
- VERSION=${GITHUB_REF#refs/tags/}
+ VERSION=${GITHUB_REF_NAME#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Extrair notas do CHANGELOG.md
@@ -86,7 +80,7 @@ jobs:
type=raw,value=latest
- name: Build and push
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: .
push: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fa2297..378021c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
## [Unreleased]
+### Adicionado
+- Projetos, canvases versionados e artefatos persistidos, com compartilhamento, histórico de versões e respectivas migrações de banco.
+- Bastidores das respostas de chat com roteamento, fallback, TTFT, estimativa de tokens por segundo e notas em reações.
+
+### Segurança
+- Dependências transitivas corrigidas para eliminar os advisories conhecidos dos grafos completo e de produção.
+
### Planejado
- Suporte a mais provedores (Perplexity, Together AI)
- Sistema de plugins
diff --git a/CLAUDE.md b/CLAUDE.md
index 95f03f3..2fbf027 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -72,3 +72,13 @@ GitHub Actions runs lint, typecheck, test, and build in parallel on push/PR to m
- 2-space indentation, single quotes, semicolons (Prettier configured in `.prettierrc`)
- Prefer Server Components; use `"use client"` only when needed
- Tests colocated with source files (e.g., `lib/chat-stream.test.ts`)
+
+
+
+# This is NOT the Next.js you know
+
+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.
+
+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.
+
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..c56eafb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,67 @@
+FROM node:22-bookworm-slim AS base
+
+ENV NEXT_TELEMETRY_DISABLED=1 \
+ PUPPETEER_SKIP_DOWNLOAD=true
+
+WORKDIR /app
+
+RUN corepack enable \
+ && corepack prepare pnpm@10.33.0 --activate
+
+
+FROM base AS dependencies
+
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN pnpm install --frozen-lockfile --ignore-scripts
+
+
+FROM base AS production-dependencies
+
+ENV NODE_ENV=production
+
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN pnpm install --prod --frozen-lockfile --ignore-scripts
+
+
+FROM base AS builder
+
+ENV NODE_ENV=production
+
+COPY --from=dependencies /app/node_modules ./node_modules
+COPY . .
+
+# Prisma 7 writes the client to generated/prisma. Generation does not require
+# a live database; prisma.config.ts supplies a non-secret fallback URL here.
+RUN pnpm prisma:generate \
+ && pnpm build
+
+
+FROM node:22-bookworm-slim AS runner
+
+ENV NODE_ENV=production \
+ NEXT_TELEMETRY_DISABLED=1 \
+ PUPPETEER_SKIP_DOWNLOAD=true \
+ PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
+ HOSTNAME=0.0.0.0 \
+ PORT=3000
+
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y ca-certificates chromium \
+ && rm -rf /var/lib/apt/lists/* \
+ && mkdir -p /app/logs \
+ && chown node:node /app/logs
+
+COPY --from=production-dependencies --chown=node:node /app/node_modules ./node_modules
+COPY --from=builder --chown=node:node /app/.next ./.next
+COPY --from=builder --chown=node:node /app/generated ./generated
+COPY --from=builder --chown=node:node /app/public ./public
+COPY --from=builder --chown=node:node /app/next.config.ts ./next.config.ts
+COPY --from=builder --chown=node:node /app/package.json ./package.json
+
+USER node
+
+EXPOSE 3000
+
+CMD ["node_modules/.bin/next", "start"]
diff --git a/README.md b/README.md
index 9202ad4..4f8b1a6 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -56,6 +56,10 @@ Em vez de cada aplicação integrar vários provedores separadamente, o ModelHub
Roteamento inteligente Tiers por complexidade, overrides por tarefa e fallbacks automáticos. |
Anexos no chat Suporte a imagens, PDFs e documentos. |
+
+ Canvas versionado Edite, visualize, restaure e compartilhe conteúdo Markdown, código, HTML, React e Mermaid. |
+ Projetos Agrupe conversas, instruções, arquivos de conhecimento e artefatos reutilizáveis. |
+
Catálogo dinâmico Modelos locais e busca remota quando o provider suporta. |
Pronto para produção Rate limit, cooldown, headers de segurança, CI e deploy na Vercel. |
@@ -200,6 +204,7 @@ O campo `model` segue o formato `provider/model`, por exemplo:
| Rota | Descrição |
|---|---|
| `/chat` | Conversa com provedores configurados |
+| `/projects` | Projetos, arquivos de conhecimento e artefatos de canvas |
| `/setup` | Integrações e credenciais por provider |
| `/dashboard` | API keys, uso, custos, logs e routing |
| `/account` | Informações da conta |
@@ -240,7 +245,7 @@ A aplicação usa duas camadas:
O banco é PostgreSQL via Neon, acessado com Prisma 7 e `@prisma/adapter-neon`.
-Modelos importantes: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `UsageLog`, `UserMemory` e `UserSettings`.
+Modelos importantes: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `Project`, `ProjectFile`, `ProjectArtifact`, `Canvas`, `UsageLog`, `UserMemory` e `UserSettings`.
Para mudanças de schema:
diff --git a/README_EN.md b/README_EN.md
index 9648ccb..42116a8 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -11,7 +11,7 @@
-
+
@@ -56,6 +56,10 @@ Instead of each application integrating multiple providers separately, ModelHub
Smart routing Tiers by complexity, per-task overrides, and automatic fallbacks. |
Chat attachments Support for images, PDFs, and documents. |
+
+ Versioned canvas Edit, preview, restore, and share Markdown, code, HTML, React, and Mermaid content. |
+ Projects Group conversations, instructions, knowledge files, and reusable artifacts. |
+
Dynamic catalog Local models and remote search when the provider supports it. |
Production-ready Rate limiting, cooldown, security headers, CI, and Vercel deploy. |
@@ -200,6 +204,7 @@ The `model` field follows the `provider/model` format, for example:
| Route | Description |
|---|---|
| `/chat` | Chat with configured providers |
+| `/projects` | Projects, knowledge files, and canvas artifacts |
| `/setup` | Integrations and credentials per provider |
| `/dashboard` | API keys, usage, costs, logs, and routing |
| `/account` | Account information |
@@ -240,7 +245,7 @@ The application uses two layers:
The database is PostgreSQL via Neon, accessed with Prisma 7 and `@prisma/adapter-neon`.
-Key models: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `UsageLog`, `UserMemory`, and `UserSettings`.
+Key models: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `Project`, `ProjectFile`, `ProjectArtifact`, `Canvas`, `UsageLog`, `UserMemory`, and `UserSettings`.
For schema changes:
diff --git a/app/(app)/projects/[id]/page.tsx b/app/(app)/projects/[id]/page.tsx
index 81ab244..b43507e 100644
--- a/app/(app)/projects/[id]/page.tsx
+++ b/app/(app)/projects/[id]/page.tsx
@@ -1,9 +1,10 @@
import { ProjectDetailPage } from "@/components/projects/project-detail-page";
-export default function ProjectRoutePage({
+export default async function ProjectRoutePage({
params,
}: {
params: Promise<{ id: string }>;
}) {
- return ;
+ const { id } = await params;
+ return ;
}
diff --git a/app/accent-provider.tsx b/app/accent-provider.tsx
index e61c8e2..e1710e6 100644
--- a/app/accent-provider.tsx
+++ b/app/accent-provider.tsx
@@ -1,6 +1,6 @@
"use client";
-import { createContext, useCallback, useContext, useEffect, useState } from "react";
+import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import { apiJson } from "@/lib/api";
import { isValidAccentColor, type AccentColorId } from "@/lib/accent-colors";
@@ -30,20 +30,37 @@ function applyAccentToDocument(accent: AccentColorId | null) {
*/
export function AccentProvider({ children }: { children: React.ReactNode }) {
const [accent, setAccentState] = useState(null);
+ const localChangeVersionRef = useRef(0);
useEffect(() => {
let cancelled = false;
async function loadAccent() {
+ const loadVersion = localChangeVersionRef.current;
+ const locallyApplied = document.documentElement.getAttribute("data-accent");
+ if (isValidAccentColor(locallyApplied)) {
+ setAccentState(locallyApplied);
+ }
+
try {
const data = await apiJson<{ settings?: { accentColor?: string | null } }>(
"/user/settings",
);
- if (cancelled) return;
- const stored = data.settings?.accentColor;
- if (isValidAccentColor(stored)) {
- setAccentState(stored);
- applyAccentToDocument(stored);
+ if (cancelled || loadVersion !== localChangeVersionRef.current) return;
+ if (data.settings && "accentColor" in data.settings) {
+ const stored = data.settings.accentColor;
+ const next = isValidAccentColor(stored) ? stored : "default";
+ setAccentState(next);
+ applyAccentToDocument(next);
+ try {
+ if (next === "default") {
+ window.localStorage.removeItem(ACCENT_STORAGE_KEY);
+ } else {
+ window.localStorage.setItem(ACCENT_STORAGE_KEY, next);
+ }
+ } catch {
+ // localStorage bloqueado: o valor do servidor ainda vale nesta aba.
+ }
}
} catch {
// Sem sessão ou offline: mantém o que o script inline aplicou.
@@ -57,6 +74,7 @@ export function AccentProvider({ children }: { children: React.ReactNode }) {
}, []);
const setAccent = useCallback((next: AccentColorId | null) => {
+ localChangeVersionRef.current += 1;
setAccentState(next);
applyAccentToDocument(next);
try {
diff --git a/app/layout.tsx b/app/layout.tsx
index 126d926..79ddd0d 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Inter, JetBrains_Mono, Source_Serif_4 } from "next/font/google";
+import Script from "next/script";
import { Analytics } from "@vercel/analytics/next";
import { Providers } from "./providers";
@@ -55,7 +56,9 @@ export default function RootLayout({
>
{/* Aplica o accent salvo antes da hidratação para evitar flash (issue #177). */}
- ok