Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,6 +41,7 @@ Thumbs.db
# Testing
coverage
.nyc_output
.playwright-cli
*.lcov

# Logs
Expand Down Expand Up @@ -81,3 +83,4 @@ temp
# Monorepo
apps
packages
manifest-main
10 changes: 2 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

<!-- BEGIN:nextjs-agent-rules -->

# 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.

<!-- END:nextjs-agent-rules -->
67 changes: 67 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<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>
<a href="LICENSE"><img alt="Licença MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
<a href="https://nodejs.org"><img alt="Node.js >= 22" src="https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen" /></a>
<a href="https://nextjs.org/"><img alt="Next.js 16.2" src="https://img.shields.io/badge/Next.js-16.2-black" /></a>
<a href="https://nextjs.org/"><img alt="Next.js 16.3" src="https://img.shields.io/badge/Next.js-16.3-black" /></a>
<a href="https://www.typescriptlang.org/"><img alt="TypeScript 5" src="https://img.shields.io/badge/TypeScript-5.x-blue" /></a>
<a href="https://hono.dev/"><img alt="Hono 4" src="https://img.shields.io/badge/Hono-4.x-E36002?logo=hono&logoColor=white" /></a>
<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>
Expand Down Expand Up @@ -56,6 +56,10 @@ Em vez de cada aplicação integrar vários provedores separadamente, o ModelHub
<td><strong>Roteamento inteligente</strong><br />Tiers por complexidade, overrides por tarefa e fallbacks automáticos.</td>
<td><strong>Anexos no chat</strong><br />Suporte a imagens, PDFs e documentos.</td>
</tr>
<tr>
<td><strong>Canvas versionado</strong><br />Edite, visualize, restaure e compartilhe conteúdo Markdown, código, HTML, React e Mermaid.</td>
<td><strong>Projetos</strong><br />Agrupe conversas, instruções, arquivos de conhecimento e artefatos reutilizáveis.</td>
</tr>
<tr>
<td><strong>Catálogo dinâmico</strong><br />Modelos locais e busca remota quando o provider suporta.</td>
<td><strong>Pronto para produção</strong><br />Rate limit, cooldown, headers de segurança, CI e deploy na Vercel.</td>
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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:

Expand Down
9 changes: 7 additions & 2 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<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>
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
<a href="https://nodejs.org"><img alt="Node.js >= 22" src="https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen" /></a>
<a href="https://nextjs.org/"><img alt="Next.js 16.2" src="https://img.shields.io/badge/Next.js-16.2-black" /></a>
<a href="https://nextjs.org/"><img alt="Next.js 16.3" src="https://img.shields.io/badge/Next.js-16.3-black" /></a>
<a href="https://www.typescriptlang.org/"><img alt="TypeScript 5" src="https://img.shields.io/badge/TypeScript-5.x-blue" /></a>
<a href="https://hono.dev/"><img alt="Hono 4" src="https://img.shields.io/badge/Hono-4.x-E36002?logo=hono&logoColor=white" /></a>
<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>
Expand Down Expand Up @@ -56,6 +56,10 @@ Instead of each application integrating multiple providers separately, ModelHub
<td><strong>Smart routing</strong><br />Tiers by complexity, per-task overrides, and automatic fallbacks.</td>
<td><strong>Chat attachments</strong><br />Support for images, PDFs, and documents.</td>
</tr>
<tr>
<td><strong>Versioned canvas</strong><br />Edit, preview, restore, and share Markdown, code, HTML, React, and Mermaid content.</td>
<td><strong>Projects</strong><br />Group conversations, instructions, knowledge files, and reusable artifacts.</td>
</tr>
<tr>
<td><strong>Dynamic catalog</strong><br />Local models and remote search when the provider supports it.</td>
<td><strong>Production-ready</strong><br />Rate limiting, cooldown, security headers, CI, and Vercel deploy.</td>
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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:

Expand Down
5 changes: 3 additions & 2 deletions app/(app)/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProjectDetailPage projectIdPromise={params} />;
const { id } = await params;
return <ProjectDetailPage projectId={id} />;
}
30 changes: 24 additions & 6 deletions app/accent-provider.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -30,20 +30,37 @@ function applyAccentToDocument(accent: AccentColorId | null) {
*/
export function AccentProvider({ children }: { children: React.ReactNode }) {
const [accent, setAccentState] = useState<AccentColorId | null>(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.
Expand All @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -55,7 +56,9 @@ export default function RootLayout({
>
<head>
{/* Aplica o accent salvo antes da hidratação para evitar flash (issue #177). */}
<script
<Script
id="modelhub-accent"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__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){}})();`,
}}
Expand Down
Loading
Loading