d34c722167
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
234 lines
6.4 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|