Files
ArchiTools/src/app/api/manictime/route.ts
T
AI Assistant d34c722167 feat(3.15): AI Tools extindere si integrare
Prompt Generator:
- Search bar cu cautare in name/description/tags/category
- Filtru target type (text/image) cu toggle rapid 'Imagine'
- 4 template-uri noi imagine: Midjourney Exterior, SD Interior,
  Midjourney Infographic, SD Material Texture (18 total)
- Config v0.2.0

AI Chat  Real API Integration:
- /api/ai-chat route: multi-provider (OpenAI, Anthropic, Ollama, demo)
- System prompt default in romana pt context arhitectura
- GET: config status, POST: message routing
- use-chat.ts: sendMessage() cu fetch real, sending state,
  providerConfig fetch, updateSession() pt project linking
- UI: provider status badge (Wifi/WifiOff), Bot icon pe mesaje,
  loading spinner la generare, disable input while sending
- Config banner cu detalii provider/model/stare

AI Chat + Tag Manager:
- Project selector dropdown in chat header (useTags project)
- Session linking: projectTagId + projectName on ChatSession
- Project name display in session sidebar
- Project context injected in system prompt

Docker:
- AI env vars: AI_PROVIDER, AI_API_KEY, AI_MODEL, AI_BASE_URL, AI_MAX_TOKENS
2026-02-28 04:51:36 +02:00

234 lines
6.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { promises as fs } from "fs";
import path from "path";
import {
parseManicTimeFile,
serializeToManicTime,
computeSyncPlan,
generateBackupFilename,
validateHierarchy,
manicTimeTagToCreateData,
} from "@/modules/tag-manager/services/manictime-service";
import type { Tag } from "@/core/tagging/types";
import { prisma } from "@/core/storage/prisma";
import type { Prisma } from "@prisma/client";
const NAMESPACE = "tags";
/**
* Get the ManicTime Tags.txt file path from environment.
* Default: /mnt/manictime/Tags.txt (Docker SMB mount)
*/
function getTagsFilePath(): string {
return process.env.MANICTIME_TAGS_PATH ?? "/mnt/manictime/Tags.txt";
}
/**
* Load all tags from the database (same namespace as TagService).
*/
async function loadArchiToolsTags(): Promise<Tag[]> {
const records = await prisma.keyValueStore.findMany({
where: { namespace: NAMESPACE },
});
return records.map((r) => r.value as unknown as Tag);
}
/**
* GET /api/manictime
* Read and parse the ManicTime Tags.txt file.
* Returns parsed tags + sync plan against ArchiTools.
*/
export async function GET() {
const filePath = getTagsFilePath();
try {
await fs.access(filePath);
} catch {
return NextResponse.json(
{
error: "file_not_found",
message: `Fișierul Tags.txt nu a fost găsit la: ${filePath}. Verificați montarea SMB și variabila MANICTIME_TAGS_PATH.`,
filePath,
},
{ status: 404 },
);
}
try {
const content = await fs.readFile(filePath, "utf-8");
const parsed = parseManicTimeFile(content);
const archiToolsTags = await loadArchiToolsTags();
const syncPlan = computeSyncPlan(parsed.tags, archiToolsTags);
return NextResponse.json({
filePath,
lastModified: (await fs.stat(filePath)).mtime.toISOString(),
totalLines: parsed.tags.length,
groups: parsed.groups.length,
syncPlan: {
addToArchiTools: syncPlan.addedToArchiTools.length,
addToManicTime: syncPlan.addedToManicTime.length,
unchanged: syncPlan.unchanged,
conflicts: syncPlan.conflicts,
},
manicTimeTags: parsed.tags,
archiToolsCount: archiToolsTags.length,
});
} catch (error) {
return NextResponse.json(
{
error: "read_error",
message: `Eroare la citirea fișierului: ${error instanceof Error ? error.message : "necunoscută"}`,
},
{ status: 500 },
);
}
}
/**
* POST /api/manictime
* Perform bidirectional sync.
*
* Body: { direction: 'pull' | 'push' | 'both' }
* - pull: Import new tags from ManicTime → ArchiTools
* - push: Export ArchiTools tags → ManicTime Tags.txt (with backup)
* - both: Pull then push
*/
export async function POST(request: NextRequest) {
const filePath = getTagsFilePath();
let body: { direction?: string } = {};
try {
body = (await request.json()) as { direction?: string };
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}
const direction = body.direction ?? "both";
if (!["pull", "push", "both"].includes(direction)) {
return NextResponse.json(
{
error: "invalid_direction",
message: "Direction must be pull, push, or both",
},
{ status: 400 },
);
}
try {
const archiToolsTags = await loadArchiToolsTags();
let pulled = 0;
let pushed = false;
// ── Pull: ManicTime → ArchiTools ──
if (direction === "pull" || direction === "both") {
let content: string;
try {
content = await fs.readFile(filePath, "utf-8");
} catch {
return NextResponse.json(
{
error: "file_not_found",
message: `Fișierul Tags.txt nu a fost găsit la: ${filePath}`,
},
{ status: 404 },
);
}
const parsed = parseManicTimeFile(content);
const syncPlan = computeSyncPlan(parsed.tags, archiToolsTags);
// Import new tags into ArchiTools DB
const existingKeys = new Set(
archiToolsTags.map((t) => `${t.category}::${t.label.toLowerCase()}`),
);
for (const mt of syncPlan.addedToArchiTools) {
const data = manicTimeTagToCreateData(mt);
if (!data) continue;
const key = `${data.category}::${data.label.toLowerCase()}`;
if (existingKeys.has(key)) continue;
const id = crypto.randomUUID();
const tag: Tag = {
...data,
id,
createdAt: new Date().toISOString(),
};
const tagJson = JSON.parse(
JSON.stringify(tag),
) as Prisma.InputJsonValue;
await prisma.keyValueStore.upsert({
where: { namespace_key: { namespace: NAMESPACE, key: id } },
update: { value: tagJson },
create: {
namespace: NAMESPACE,
key: id,
value: tagJson,
},
});
existingKeys.add(key);
pulled++;
}
}
// ── Push: ArchiTools → ManicTime ──
if (direction === "push" || direction === "both") {
// Re-load to include any newly pulled tags
const allTags = await loadArchiToolsTags();
// Validate hierarchy before writing
const warnings = validateHierarchy(allTags);
// Create backup if file exists
try {
await fs.access(filePath);
const backupPath = generateBackupFilename(filePath);
await fs.copyFile(filePath, backupPath);
} catch {
// File doesn't exist yet, no backup needed
}
// Ensure directory exists
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
// Write new content
const newContent = serializeToManicTime(allTags);
await fs.writeFile(filePath, newContent, "utf-8");
pushed = true;
return NextResponse.json({
success: true,
direction,
pulled,
pushed,
totalTags: allTags.length,
warnings,
timestamp: new Date().toISOString(),
});
}
return NextResponse.json({
success: true,
direction,
pulled,
pushed,
totalTags: archiToolsTags.length + pulled,
warnings: [],
timestamp: new Date().toISOString(),
});
} catch (error) {
return NextResponse.json(
{
error: "sync_error",
message: `Eroare la sincronizare: ${error instanceof Error ? error.message : "necunoscută"}`,
},
{ status: 500 },
);
}
}