Team/user CRMFS export + scoped chat
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,9 +3,10 @@ node_modules
|
||||
.nuxt
|
||||
.output
|
||||
.data
|
||||
.env
|
||||
Frontend/.data
|
||||
dist
|
||||
coverage
|
||||
npm-debug.log*
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
|
||||
1
Frontend/.env.example
Normal file
1
Frontend/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
DATABASE_URL="file:../../.data/clientsflow-dev.db"
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
type TabId = "communications" | "documents";
|
||||
type CalendarView = "day" | "week" | "month" | "year" | "agenda";
|
||||
type SortMode = "name" | "lastContact";
|
||||
@@ -493,26 +494,42 @@ const documents = ref<WorkspaceDocument[]>([
|
||||
},
|
||||
]);
|
||||
|
||||
const pilotMessages = ref([
|
||||
{ id: "p1", role: "assistant", text: "I monitor calendar, contacts, and communications in one flow." },
|
||||
{ id: "p2", role: "user", text: "What is critical to do today?" },
|
||||
{ id: "p3", role: "assistant", text: "First Anna after demo, then Murat on legal owner." },
|
||||
]);
|
||||
type PilotMessage = {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
text: string;
|
||||
plan?: string[] | null;
|
||||
tools?: string[] | null;
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
const pilotMessages = ref<PilotMessage[]>([]);
|
||||
const pilotInput = ref("");
|
||||
const pilotSending = ref(false);
|
||||
|
||||
function sendPilotMessage() {
|
||||
const text = pilotInput.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
pilotMessages.value.push({ id: `p-${Date.now()}`, role: "user", text });
|
||||
pilotMessages.value.push({
|
||||
id: `p-${Date.now() + 1}`,
|
||||
role: "assistant",
|
||||
text: "Got it. Added to context and will use it in the next recommendations.",
|
||||
});
|
||||
pilotInput.value = "";
|
||||
async function loadPilotMessages() {
|
||||
const res = await $fetch<{ items: PilotMessage[] }>("/api/chat");
|
||||
pilotMessages.value = res.items ?? [];
|
||||
}
|
||||
|
||||
async function sendPilotMessage() {
|
||||
const text = pilotInput.value.trim();
|
||||
if (!text || pilotSending.value) return;
|
||||
|
||||
pilotSending.value = true;
|
||||
try {
|
||||
await $fetch("/api/chat", { method: "POST", body: { text } });
|
||||
pilotInput.value = "";
|
||||
await loadPilotMessages();
|
||||
} finally {
|
||||
pilotSending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPilotMessages();
|
||||
});
|
||||
|
||||
const calendarView = ref<CalendarView>("month");
|
||||
const calendarCursor = ref(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||
const selectedDateKey = ref(dayKey(new Date()));
|
||||
@@ -1117,7 +1134,10 @@ function makeId(prefix: string) {
|
||||
}
|
||||
|
||||
function pushPilotNote(text: string) {
|
||||
pilotMessages.value.push({ id: makeId("p"), role: "assistant", text });
|
||||
// Fire-and-forget: log assistant note to the same conversation.
|
||||
$fetch("/api/chat/log", { method: "POST", body: { text } })
|
||||
.then(loadPilotMessages)
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function openCommunicationThread(contact: string) {
|
||||
@@ -1269,8 +1289,31 @@ function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {
|
||||
class="chat"
|
||||
:class="message.role === 'assistant' ? 'chat-start' : 'chat-end'"
|
||||
>
|
||||
<div class="chat-bubble text-sm" :class="message.role === 'assistant' ? 'chat-bubble-neutral' : ''">
|
||||
{{ message.text }}
|
||||
<div class="space-y-2">
|
||||
<div class="chat-bubble text-sm" :class="message.role === 'assistant' ? 'chat-bubble-neutral' : ''">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
|
||||
<details
|
||||
v-if="message.role === 'assistant' && ((message.plan && message.plan.length) || (message.tools && message.tools.length))"
|
||||
class="rounded-lg border border-base-300 bg-base-100 p-2 text-xs"
|
||||
>
|
||||
<summary class="cursor-pointer select-none font-semibold text-base-content/70">Plan & tools</summary>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div v-if="message.plan && message.plan.length">
|
||||
<div class="font-semibold text-base-content/70">Plan</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="(step, idx) in message.plan" :key="`plan-${message.id}-${idx}`">{{ step }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="message.tools && message.tools.length">
|
||||
<div class="font-semibold text-base-content/70">Tools</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="(tool, idx) in message.tools" :key="`tools-${message.id}-${idx}`">{{ tool }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
732
Frontend/package-lock.json
generated
732
Frontend/package-lock.json
generated
@@ -7,6 +7,10 @@
|
||||
"name": "crm-frontend",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.77",
|
||||
"@langchain/langgraph": "^0.2.74",
|
||||
"@langchain/openai": "^0.6.9",
|
||||
"@prisma/client": "^6.16.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tiptap/extension-collaboration": "^2.27.2",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.27.2",
|
||||
@@ -18,7 +22,11 @@
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.27",
|
||||
"y-webrtc": "^10.3.0",
|
||||
"yjs": "^13.6.29"
|
||||
"yjs": "^13.6.29",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^6.16.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -443,6 +451,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.1.tgz",
|
||||
@@ -1036,6 +1050,186 @@
|
||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "0.3.80",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.80.tgz",
|
||||
"integrity": "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": "^0.3.67",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "4",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.25.32",
|
||||
"zod-to-json-schema": "^3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph": {
|
||||
"version": "0.2.74",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz",
|
||||
"integrity": "sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph-checkpoint": "~0.0.17",
|
||||
"@langchain/langgraph-sdk": "~0.0.32",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0",
|
||||
"zod-to-json-schema": "^3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod-to-json-schema": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz",
|
||||
"integrity": "sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.31 <0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk": {
|
||||
"version": "0.0.112",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.112.tgz",
|
||||
"integrity": "sha512-/9W5HSWCqYgwma6EoOspL4BGYxGxeJP6lIquPSF4FA0JlKopaUv58ucZC3vAgdJyCgg6sorCIV/qg7SGpEcCLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "4",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.31 <0.4.0",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai": {
|
||||
"version": "0.6.17",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.6.17.tgz",
|
||||
"integrity": "sha512-JVSzD+FL5v/2UQxKd+ikB1h4PQOtn0VlK8nqW2kPp0fshItCv4utrjBKXC/rubBnSXoRTyonBINe8QRZ6OojVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"openai": "5.12.2",
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.3.68 <0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai/node_modules/openai": {
|
||||
"version": "5.12.2",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz",
|
||||
"integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz",
|
||||
@@ -2786,6 +2980,199 @@
|
||||
"integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz",
|
||||
"integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*",
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz",
|
||||
"integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"c12": "3.1.0",
|
||||
"deepmerge-ts": "7.1.5",
|
||||
"effect": "3.18.4",
|
||||
"empathic": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/c12": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"confbox": "^0.2.2",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.6.1",
|
||||
"exsolve": "^1.0.7",
|
||||
"giget": "^2.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^2.2.0",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "^0.3.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"magicast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/giget": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.0",
|
||||
"defu": "^6.1.4",
|
||||
"node-fetch-native": "^1.6.6",
|
||||
"nypm": "^0.6.0",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/rc9": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
|
||||
"integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz",
|
||||
"integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.2",
|
||||
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"@prisma/fetch-engine": "6.19.2",
|
||||
"@prisma/get-platform": "6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz",
|
||||
"integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.2",
|
||||
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"@prisma/get-platform": "6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz",
|
||||
"integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@remirror/core-constants": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||
@@ -3327,6 +3714,13 @@
|
||||
"integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
@@ -4024,6 +4418,12 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
@@ -4052,6 +4452,18 @@
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@unhead/vue": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.4.tgz",
|
||||
@@ -5116,6 +5528,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||
@@ -5148,6 +5572,34 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
@@ -5300,6 +5752,15 @@
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/console-table-printer": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz",
|
||||
"integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"simple-wcswidth": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -5665,6 +6126,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -5674,6 +6144,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge-ts": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
|
||||
@@ -5877,6 +6357,17 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "3.18.4",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
|
||||
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"fast-check": "^3.23.1"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.286",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
||||
@@ -5889,6 +6380,16 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/empathic": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -6042,6 +6543,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -6089,6 +6596,29 @@
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
@@ -6399,6 +6929,15 @@
|
||||
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -6831,6 +7370,15 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -6885,6 +7433,40 @@
|
||||
"integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.3.87",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.87.tgz",
|
||||
"integrity": "sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"console-table-printer": "^2.12.1",
|
||||
"p-queue": "^6.6.2",
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
|
||||
@@ -7625,6 +8207,15 @@
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
@@ -8225,6 +8816,56 @@
|
||||
"oxc-parser": ">=0.98.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -8815,6 +9456,32 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
|
||||
"integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.19.2",
|
||||
"@prisma/engines": "6.19.2"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
@@ -9059,6 +9726,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
@@ -9262,6 +9946,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -9633,6 +10326,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-wcswidth": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
|
||||
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
@@ -10621,6 +11320,19 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
@@ -11345,6 +12057,24 @@
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
|
||||
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,22 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:push": "prisma db push",
|
||||
"db:seed": "node prisma/seed.mjs",
|
||||
"dataset:export": "node scripts/export-dataset.mjs",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"postinstall": "nuxt prepare && prisma generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.1",
|
||||
"@langchain/core": "^0.3.77",
|
||||
"@langchain/langgraph": "^0.2.74",
|
||||
"@langchain/openai": "^0.6.9",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tiptap/extension-collaboration": "^2.27.2",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.27.2",
|
||||
@@ -21,6 +31,13 @@
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.27",
|
||||
"y-webrtc": "^10.3.0",
|
||||
"yjs": "^13.6.29"
|
||||
"yjs": "^13.6.29",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^6.16.1"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "node prisma/seed.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
169
Frontend/prisma/schema.prisma
Normal file
169
Frontend/prisma/schema.prisma
Normal file
@@ -0,0 +1,169 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum TeamRole {
|
||||
OWNER
|
||||
MEMBER
|
||||
}
|
||||
|
||||
enum MessageDirection {
|
||||
IN
|
||||
OUT
|
||||
}
|
||||
|
||||
enum MessageChannel {
|
||||
TELEGRAM
|
||||
WHATSAPP
|
||||
INSTAGRAM
|
||||
PHONE
|
||||
EMAIL
|
||||
INTERNAL
|
||||
}
|
||||
|
||||
enum ChatRole {
|
||||
USER
|
||||
ASSISTANT
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
members TeamMember[]
|
||||
contacts Contact[]
|
||||
calendarEvents CalendarEvent[]
|
||||
conversations ChatConversation[]
|
||||
chatMessages ChatMessage[]
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
memberships TeamMember[]
|
||||
conversations ChatConversation[] @relation("ConversationCreator")
|
||||
chatMessages ChatMessage[] @relation("ChatAuthor")
|
||||
}
|
||||
|
||||
model TeamMember {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
userId String
|
||||
role TeamRole @default(MEMBER)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([teamId, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
name String
|
||||
company String?
|
||||
email String?
|
||||
phone String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
note ContactNote?
|
||||
messages ContactMessage[]
|
||||
events CalendarEvent[]
|
||||
|
||||
@@index([teamId, updatedAt])
|
||||
}
|
||||
|
||||
model ContactNote {
|
||||
id String @id @default(cuid())
|
||||
contactId String @unique
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model ContactMessage {
|
||||
id String @id @default(cuid())
|
||||
contactId String
|
||||
direction MessageDirection
|
||||
channel MessageChannel
|
||||
content String
|
||||
occurredAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([contactId, occurredAt])
|
||||
}
|
||||
|
||||
model CalendarEvent {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
contactId String?
|
||||
title String
|
||||
startsAt DateTime
|
||||
endsAt DateTime?
|
||||
note String?
|
||||
status String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([startsAt])
|
||||
@@index([contactId, startsAt])
|
||||
@@index([teamId, startsAt])
|
||||
}
|
||||
|
||||
model ChatConversation {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
createdByUserId String
|
||||
title String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
createdByUser User @relation("ConversationCreator", fields: [createdByUserId], references: [id], onDelete: Cascade)
|
||||
messages ChatMessage[]
|
||||
|
||||
@@index([teamId, updatedAt])
|
||||
@@index([createdByUserId])
|
||||
}
|
||||
|
||||
model ChatMessage {
|
||||
id String @id @default(cuid())
|
||||
teamId String
|
||||
conversationId String
|
||||
authorUserId String?
|
||||
role ChatRole
|
||||
text String
|
||||
planJson Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
authorUser User? @relation("ChatAuthor", fields: [authorUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([teamId, createdAt])
|
||||
@@index([conversationId, createdAt])
|
||||
}
|
||||
215
Frontend/prisma/seed.mjs
Normal file
215
Frontend/prisma/seed.mjs
Normal file
@@ -0,0 +1,215 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function loadEnvFromDotEnv() {
|
||||
const p = path.resolve(process.cwd(), ".env");
|
||||
if (!fs.existsSync(p)) return;
|
||||
const raw = fs.readFileSync(p, "utf8");
|
||||
for (const line of raw.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const idx = trimmed.indexOf("=");
|
||||
if (idx === -1) continue;
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
let val = trimmed.slice(idx + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
if (!key) continue;
|
||||
// Force DATABASE_URL from local .env for scripts, to avoid inheriting a stale shell env.
|
||||
if (key === "DATABASE_URL") {
|
||||
process.env[key] = val;
|
||||
continue;
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFromDotEnv();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function atOffset(days, hour, minute) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + days);
|
||||
d.setHours(hour, minute, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create default team/user for dev.
|
||||
const user = await prisma.user.upsert({
|
||||
where: { id: "demo-user" },
|
||||
update: { email: "demo@clientsflow.local", name: "Demo User" },
|
||||
create: { id: "demo-user", email: "demo@clientsflow.local", name: "Demo User" },
|
||||
});
|
||||
|
||||
const team = await prisma.team.upsert({
|
||||
where: { id: "demo-team" },
|
||||
update: { name: "Demo Team" },
|
||||
create: { id: "demo-team", name: "Demo Team" },
|
||||
});
|
||||
|
||||
await prisma.teamMember.upsert({
|
||||
where: { teamId_userId: { teamId: team.id, userId: user.id } },
|
||||
update: {},
|
||||
create: { teamId: team.id, userId: user.id, role: "OWNER" },
|
||||
});
|
||||
|
||||
// Idempotent-ish seed per team: if we already have contacts in this team, do nothing.
|
||||
const existing = await prisma.contact.count({ where: { teamId: team.id } });
|
||||
if (existing > 0) return;
|
||||
|
||||
const contacts = await prisma.contact.createManyAndReturn({
|
||||
data: [
|
||||
{
|
||||
teamId: team.id,
|
||||
name: "Anna Meyer",
|
||||
company: "Nordline GmbH",
|
||||
email: "anna@nordline.example",
|
||||
phone: "+49 30 123 45 67",
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
name: "Murat Ali",
|
||||
company: "Connect FZCO",
|
||||
email: "murat@connect.example",
|
||||
phone: "+971 50 123 4567",
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
name: "Ilya Petroff",
|
||||
company: "Volta Tech",
|
||||
email: "ilya@volta.example",
|
||||
phone: "+374 10 123 456",
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
name: "Carlos Rivera",
|
||||
company: "BluePort",
|
||||
email: "carlos@blueport.example",
|
||||
phone: "+34 600 123 456",
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
name: "Daria Ivanova",
|
||||
company: "Skyline Trade",
|
||||
email: "daria@skyline.example",
|
||||
phone: "+7 777 123 45 67",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const byName = Object.fromEntries(contacts.map((c) => [c.name, c]));
|
||||
|
||||
await prisma.contactNote.createMany({
|
||||
data: [
|
||||
{
|
||||
contactId: byName["Anna Meyer"].id,
|
||||
content:
|
||||
"Decision owner. Prefers short, concrete updates with a clear next step.\nRisk: decision date slips if we don't lock timeline.",
|
||||
},
|
||||
{
|
||||
contactId: byName["Murat Ali"].id,
|
||||
content:
|
||||
"High activity. Needs legal path clarity and an explicit owner on their side.\nBest move: lock legal owner + target signature date.",
|
||||
},
|
||||
{
|
||||
contactId: byName["Ilya Petroff"].id,
|
||||
content:
|
||||
"Early-stage. Wants structured onboarding before commercial details.\nBest move: onboarding plan + 2 time slots.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.contactMessage.createMany({
|
||||
data: [
|
||||
{
|
||||
contactId: byName["Anna Meyer"].id,
|
||||
direction: "IN",
|
||||
channel: "TELEGRAM",
|
||||
content: "Thanks for the demo. Can you send 2 pricing options?",
|
||||
occurredAt: atOffset(0, 10, 20),
|
||||
},
|
||||
{
|
||||
contactId: byName["Anna Meyer"].id,
|
||||
direction: "OUT",
|
||||
channel: "EMAIL",
|
||||
content: "Sure. Option A/B attached. Can you confirm decision date for this cycle?",
|
||||
occurredAt: atOffset(0, 10, 35),
|
||||
},
|
||||
{
|
||||
contactId: byName["Murat Ali"].id,
|
||||
direction: "IN",
|
||||
channel: "WHATSAPP",
|
||||
content: "Let's do a quick call. Need to clarify legal owner.",
|
||||
occurredAt: atOffset(-1, 18, 10),
|
||||
},
|
||||
{
|
||||
contactId: byName["Ilya Petroff"].id,
|
||||
direction: "OUT",
|
||||
channel: "EMAIL",
|
||||
content: "Draft: onboarding plan + two slots for tomorrow.",
|
||||
occurredAt: atOffset(-1, 11, 12),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.calendarEvent.createMany({
|
||||
data: [
|
||||
{
|
||||
teamId: team.id,
|
||||
contactId: byName["Anna Meyer"].id,
|
||||
title: "Follow-up: Anna",
|
||||
startsAt: atOffset(0, 12, 30),
|
||||
endsAt: atOffset(0, 13, 0),
|
||||
note: "Lock decision date + confirm option A/B.",
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
teamId: team.id,
|
||||
contactId: byName["Murat Ali"].id,
|
||||
title: "Call: Murat (legal owner)",
|
||||
startsAt: atOffset(0, 15, 0),
|
||||
endsAt: atOffset(0, 15, 20),
|
||||
note: "Confirm legal owner + target signature date.",
|
||||
status: "planned",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.chatConversation.upsert({
|
||||
where: { id: `pilot-${team.id}` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `pilot-${team.id}`,
|
||||
teamId: team.id,
|
||||
createdByUserId: user.id,
|
||||
title: "Pilot",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.chatMessage.createMany({
|
||||
data: [
|
||||
{
|
||||
teamId: team.id,
|
||||
conversationId: `pilot-${team.id}`,
|
||||
authorUserId: null,
|
||||
role: "ASSISTANT",
|
||||
text:
|
||||
"Я смотрю календарь, контакты и переписки как один поток. Спроси: \"чем заняться сегодня\" или \"покажи 10 лучших клиентов\".",
|
||||
planJson: { steps: ["Скажи задачу", "Я соберу срез данных", "Предложу план и действия"], tools: ["read index/contacts.json"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
168
Frontend/scripts/export-dataset.mjs
Normal file
168
Frontend/scripts/export-dataset.mjs
Normal file
@@ -0,0 +1,168 @@
|
||||
import fs from "node:fs/promises";
|
||||
import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
function loadEnvFromDotEnv() {
|
||||
const p = path.resolve(process.cwd(), ".env");
|
||||
if (!fsSync.existsSync(p)) return;
|
||||
const raw = fsSync.readFileSync(p, "utf8");
|
||||
for (const line of raw.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const idx = trimmed.indexOf("=");
|
||||
if (idx === -1) continue;
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
let val = trimmed.slice(idx + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
if (!key) continue;
|
||||
if (key === "DATABASE_URL") {
|
||||
process.env[key] = val;
|
||||
continue;
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFromDotEnv();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function datasetRoot() {
|
||||
const teamId = process.env.TEAM_ID || "demo-team";
|
||||
const userId = process.env.USER_ID || "demo-user";
|
||||
return path.resolve(process.cwd(), "..", ".data", "crmfs", "teams", teamId, "users", userId);
|
||||
}
|
||||
|
||||
async function ensureDir(p) {
|
||||
await fs.mkdir(p, { recursive: true });
|
||||
}
|
||||
|
||||
async function writeJson(p, value) {
|
||||
await fs.writeFile(p, JSON.stringify(value, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
function jsonlLine(value) {
|
||||
return JSON.stringify(value) + "\n";
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const root = datasetRoot();
|
||||
const tmp = root + ".tmp";
|
||||
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
await ensureDir(tmp);
|
||||
|
||||
const contactsDir = path.join(tmp, "contacts");
|
||||
const notesDir = path.join(tmp, "notes");
|
||||
const messagesDir = path.join(tmp, "messages");
|
||||
const eventsDir = path.join(tmp, "events");
|
||||
const indexDir = path.join(tmp, "index");
|
||||
await Promise.all([
|
||||
ensureDir(contactsDir),
|
||||
ensureDir(notesDir),
|
||||
ensureDir(messagesDir),
|
||||
ensureDir(eventsDir),
|
||||
ensureDir(indexDir),
|
||||
]);
|
||||
|
||||
const teamId = process.env.TEAM_ID || "demo-team";
|
||||
const contacts = await prisma.contact.findMany({
|
||||
where: { teamId },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
include: {
|
||||
note: { select: { content: true, updatedAt: true } },
|
||||
messages: {
|
||||
select: { direction: true, channel: true, content: true, occurredAt: true },
|
||||
orderBy: { occurredAt: "asc" },
|
||||
},
|
||||
events: {
|
||||
select: { title: true, startsAt: true, endsAt: true, status: true, note: true },
|
||||
orderBy: { startsAt: "asc" },
|
||||
},
|
||||
},
|
||||
take: 5000,
|
||||
});
|
||||
|
||||
const contactIndex = [];
|
||||
for (const c of contacts) {
|
||||
await writeJson(path.join(contactsDir, `${c.id}.json`), {
|
||||
id: c.id,
|
||||
teamId: c.teamId,
|
||||
name: c.name,
|
||||
company: c.company ?? null,
|
||||
email: c.email ?? null,
|
||||
phone: c.phone ?? null,
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
});
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(notesDir, `${c.id}.md`),
|
||||
(c.note?.content?.trim() ? c.note.content.trim() : "") + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(messagesDir, `${c.id}.jsonl`),
|
||||
c.messages
|
||||
.map((m) =>
|
||||
jsonlLine({
|
||||
direction: m.direction,
|
||||
channel: m.channel,
|
||||
occurredAt: m.occurredAt,
|
||||
content: m.content,
|
||||
}),
|
||||
)
|
||||
.join(""),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(eventsDir, `${c.id}.jsonl`),
|
||||
c.events
|
||||
.map((e) =>
|
||||
jsonlLine({
|
||||
title: e.title,
|
||||
startsAt: e.startsAt,
|
||||
endsAt: e.endsAt,
|
||||
status: e.status ?? null,
|
||||
note: e.note ?? null,
|
||||
}),
|
||||
)
|
||||
.join(""),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const lastMessageAt = c.messages.length ? c.messages[c.messages.length - 1].occurredAt : null;
|
||||
const nextEventAt = c.events.find((e) => new Date(e.startsAt).getTime() >= Date.now())?.startsAt ?? null;
|
||||
contactIndex.push({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
company: c.company ?? null,
|
||||
lastMessageAt,
|
||||
nextEventAt,
|
||||
updatedAt: c.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
await writeJson(path.join(indexDir, "contacts.json"), contactIndex);
|
||||
await writeJson(path.join(tmp, "meta.json"), { exportedAt: new Date().toISOString(), version: 1 });
|
||||
|
||||
await ensureDir(path.dirname(root));
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
await fs.rename(tmp, root);
|
||||
|
||||
console.log("exported", root);
|
||||
}
|
||||
|
||||
await main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
195
Frontend/server/agent/crmAgent.ts
Normal file
195
Frontend/server/agent/crmAgent.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ChatRole, Prisma } from "@prisma/client";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { datasetRoot } from "../dataset/paths";
|
||||
import { ensureDataset } from "../dataset/exporter";
|
||||
|
||||
type ContactIndexRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
company: string | null;
|
||||
lastMessageAt: string | null;
|
||||
nextEventAt: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type AgentReply = {
|
||||
text: string;
|
||||
plan: string[];
|
||||
tools: string[];
|
||||
dbWrites?: Array<{ kind: string; detail: string }>;
|
||||
};
|
||||
|
||||
function normalize(s: string) {
|
||||
return s.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isToday(date: Date) {
|
||||
const now = new Date();
|
||||
return (
|
||||
date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
async function readContactIndex(): Promise<ContactIndexRow[]> {
|
||||
throw new Error("readContactIndex now requires dataset root");
|
||||
}
|
||||
|
||||
async function readContactIndexFrom(root: string): Promise<ContactIndexRow[]> {
|
||||
const p = path.join(root, "index", "contacts.json");
|
||||
const raw = await fs.readFile(p, "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
async function countJsonlLines(p: string): Promise<number> {
|
||||
const raw = await fs.readFile(p, "utf8");
|
||||
if (!raw.trim()) return 0;
|
||||
// cheap line count (JSONL is 1 item per line)
|
||||
return raw.trimEnd().split("\n").length;
|
||||
}
|
||||
|
||||
async function readJsonl(p: string): Promise<any[]> {
|
||||
const raw = await fs.readFile(p, "utf8");
|
||||
if (!raw.trim()) return [];
|
||||
return raw
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
function formatContactLine(c: ContactIndexRow) {
|
||||
const company = c.company ? ` (${c.company})` : "";
|
||||
const lastAt = c.lastMessageAt ? new Date(c.lastMessageAt).toLocaleString("ru-RU") : "нет";
|
||||
return `- ${c.name}${company} · последнее: ${lastAt}`;
|
||||
}
|
||||
|
||||
export async function runCrmAgent(userText: string): Promise<AgentReply> {
|
||||
throw new Error("runCrmAgent now requires auth context");
|
||||
}
|
||||
|
||||
export async function runCrmAgentFor(
|
||||
input: { teamId: string; userId: string; userText: string },
|
||||
): Promise<AgentReply> {
|
||||
await ensureDataset({ teamId: input.teamId, userId: input.userId });
|
||||
const q = normalize(input.userText);
|
||||
const root = datasetRoot({ teamId: input.teamId, userId: input.userId });
|
||||
const contacts = await readContactIndexFrom(root);
|
||||
|
||||
// "10 лучших клиентов"
|
||||
if (q.includes("10 лучших") || (q.includes("топ") && q.includes("клиент"))) {
|
||||
const ranked = await Promise.all(
|
||||
contacts.map(async (c) => {
|
||||
const msgPath = path.join(root, "messages", `${c.id}.jsonl`);
|
||||
const evPath = path.join(root, "events", `${c.id}.jsonl`);
|
||||
const msgCount = await countJsonlLines(msgPath).catch(() => 0);
|
||||
const ev = await readJsonl(evPath).catch(() => []);
|
||||
const todayEvCount = ev.filter((e) => (e?.startsAt ? isToday(new Date(e.startsAt)) : false)).length;
|
||||
const score = msgCount * 2 + todayEvCount * 3;
|
||||
return { c, score };
|
||||
}),
|
||||
);
|
||||
|
||||
ranked.sort((a, b) => b.score - a.score);
|
||||
const top = ranked.slice(0, 10).map((x) => x.c);
|
||||
|
||||
return {
|
||||
plan: [
|
||||
"Загрузить индекс контактов из файлового датасета",
|
||||
"Посчитать активность по JSONL (сообщения/события сегодня)",
|
||||
"Отсортировать и показать топ",
|
||||
],
|
||||
tools: ["read index/contacts.json", "read messages/{contactId}.jsonl", "read events/{contactId}.jsonl"],
|
||||
text:
|
||||
`Топ-10 по активности (сообщения + события):\n` +
|
||||
top.map(formatContactLine).join("\n") +
|
||||
`\n\nЕсли хочешь, скажи критерий "лучший" (выручка/стадия/вероятность/давность) и я пересчитаю.`,
|
||||
};
|
||||
}
|
||||
|
||||
// "чем заняться сегодня"
|
||||
if (q.includes("чем") && (q.includes("сегодня") || q.includes("заняться"))) {
|
||||
const todayEvents: Array<{ who: string; title: string; at: Date; note?: string | null }> = [];
|
||||
|
||||
for (const c of contacts) {
|
||||
const evPath = path.join(root, "events", `${c.id}.jsonl`);
|
||||
const ev = await readJsonl(evPath).catch(() => []);
|
||||
for (const e of ev) {
|
||||
if (!e?.startsAt) continue;
|
||||
const at = new Date(e.startsAt);
|
||||
if (!isToday(at)) continue;
|
||||
todayEvents.push({ who: c.name, title: e.title ?? "Event", at, note: e.note ?? null });
|
||||
}
|
||||
}
|
||||
|
||||
todayEvents.sort((a, b) => a.at.getTime() - b.at.getTime());
|
||||
|
||||
const followups = [...contacts]
|
||||
.map((c) => ({ c, last: c.lastMessageAt ? new Date(c.lastMessageAt).getTime() : 0 }))
|
||||
.sort((a, b) => a.last - b.last)
|
||||
.slice(0, 3)
|
||||
.map((x) => x.c);
|
||||
|
||||
const lines: string[] = [];
|
||||
if (todayEvents.length > 0) {
|
||||
lines.push("Сегодня по календарю:");
|
||||
for (const e of todayEvents) {
|
||||
const hhmm = e.at.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
|
||||
lines.push(`- ${hhmm} · ${e.title} · ${e.who}${e.note ? ` · ${e.note}` : ""}`);
|
||||
}
|
||||
} else {
|
||||
lines.push("Сегодня нет запланированных событий в календаре.");
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("Фокус дня (если нужно добить прогресс):");
|
||||
for (const c of followups) {
|
||||
lines.push(`- Написать follow-up: ${c.name}${c.company ? ` (${c.company})` : ""}`);
|
||||
}
|
||||
|
||||
return {
|
||||
plan: [
|
||||
"Прочитать события на сегодня из файлового датасета",
|
||||
"Найти контакты без свежего касания (по lastMessageAt)",
|
||||
"Сформировать короткий список действий",
|
||||
],
|
||||
tools: ["read index/contacts.json", "read events/{contactId}.jsonl"],
|
||||
text: lines.join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
// Default: keep it simple, ask for intent + show what the agent can do.
|
||||
return {
|
||||
plan: ["Уточнить цель", "Выбрать данные для анализа", "Предложить план действий и, если нужно, изменения в CRM"],
|
||||
tools: ["read index/contacts.json (по необходимости)", "search messages/events (по необходимости)"],
|
||||
text:
|
||||
"Ок. Скажи, что нужно сделать.\n" +
|
||||
"Примеры:\n" +
|
||||
"- \"покажи 10 лучших клиентов\"\n" +
|
||||
"- \"чем мне сегодня заняться\"\n" +
|
||||
"- \"составь план касаний на неделю\"\n",
|
||||
};
|
||||
}
|
||||
|
||||
export async function persistChatMessage(input: {
|
||||
role: ChatRole;
|
||||
text: string;
|
||||
plan?: string[];
|
||||
tools?: string[];
|
||||
teamId: string;
|
||||
conversationId: string;
|
||||
authorUserId?: string | null;
|
||||
}) {
|
||||
const data: Prisma.ChatMessageCreateInput = {
|
||||
team: { connect: { id: input.teamId } },
|
||||
conversation: { connect: { id: input.conversationId } },
|
||||
authorUser: input.authorUserId ? { connect: { id: input.authorUserId } } : undefined,
|
||||
role: input.role,
|
||||
text: input.text,
|
||||
planJson: input.plan || input.tools ? ({ steps: input.plan ?? [], tools: input.tools ?? [] } as any) : undefined,
|
||||
};
|
||||
return prisma.chatMessage.create({ data });
|
||||
}
|
||||
22
Frontend/server/api/chat.get.ts
Normal file
22
Frontend/server/api/chat.get.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { getAuthContext } from "../utils/auth";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const auth = await getAuthContext(event);
|
||||
const items = await prisma.chatMessage.findMany({
|
||||
where: { teamId: auth.teamId, conversationId: auth.conversationId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
take: 200,
|
||||
});
|
||||
|
||||
return {
|
||||
items: items.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role === "USER" ? "user" : m.role === "ASSISTANT" ? "assistant" : "system",
|
||||
text: m.text,
|
||||
plan: (m.planJson as any)?.steps ?? null,
|
||||
tools: (m.planJson as any)?.tools ?? null,
|
||||
createdAt: m.createdAt,
|
||||
})),
|
||||
};
|
||||
});
|
||||
33
Frontend/server/api/chat.post.ts
Normal file
33
Frontend/server/api/chat.post.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { readBody } from "h3";
|
||||
import { persistChatMessage, runCrmAgentFor } from "../agent/crmAgent";
|
||||
import { getAuthContext } from "../utils/auth";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ text?: string }>(event);
|
||||
const text = (body?.text ?? "").trim();
|
||||
if (!text) {
|
||||
throw createError({ statusCode: 400, statusMessage: "text is required" });
|
||||
}
|
||||
|
||||
const auth = await getAuthContext(event);
|
||||
await persistChatMessage({
|
||||
teamId: auth.teamId,
|
||||
conversationId: auth.conversationId,
|
||||
authorUserId: auth.userId,
|
||||
role: "USER",
|
||||
text,
|
||||
});
|
||||
|
||||
const reply = await runCrmAgentFor({ teamId: auth.teamId, userId: auth.userId, userText: text });
|
||||
await persistChatMessage({
|
||||
teamId: auth.teamId,
|
||||
conversationId: auth.conversationId,
|
||||
authorUserId: null,
|
||||
role: "ASSISTANT",
|
||||
text: reply.text,
|
||||
plan: reply.plan,
|
||||
tools: reply.tools,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
25
Frontend/server/api/chat/log.post.ts
Normal file
25
Frontend/server/api/chat/log.post.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { readBody } from "h3";
|
||||
import { persistChatMessage } from "../../agent/crmAgent";
|
||||
import { getAuthContext } from "../../utils/auth";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ text?: string }>(event);
|
||||
const text = (body?.text ?? "").trim();
|
||||
if (!text) {
|
||||
throw createError({ statusCode: 400, statusMessage: "text is required" });
|
||||
}
|
||||
|
||||
const auth = await getAuthContext(event);
|
||||
await persistChatMessage({
|
||||
teamId: auth.teamId,
|
||||
conversationId: auth.conversationId,
|
||||
authorUserId: null,
|
||||
role: "ASSISTANT",
|
||||
text,
|
||||
plan: [],
|
||||
tools: [],
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
8
Frontend/server/api/dataset/export.post.ts
Normal file
8
Frontend/server/api/dataset/export.post.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { exportDatasetFromPrismaFor } from "../../dataset/exporter";
|
||||
import { getAuthContext } from "../../utils/auth";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const auth = await getAuthContext(event);
|
||||
await exportDatasetFromPrismaFor({ teamId: auth.teamId, userId: auth.userId });
|
||||
return { ok: true };
|
||||
});
|
||||
142
Frontend/server/dataset/exporter.ts
Normal file
142
Frontend/server/dataset/exporter.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { datasetRoot } from "./paths";
|
||||
|
||||
type ExportMeta = {
|
||||
exportedAt: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
async function ensureDir(p: string) {
|
||||
await fs.mkdir(p, { recursive: true });
|
||||
}
|
||||
|
||||
async function writeJson(p: string, value: unknown) {
|
||||
await fs.writeFile(p, JSON.stringify(value, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
function jsonlLine(value: unknown) {
|
||||
return JSON.stringify(value) + "\n";
|
||||
}
|
||||
|
||||
export async function exportDatasetFromPrisma() {
|
||||
throw new Error("exportDatasetFromPrisma now requires { teamId, userId }");
|
||||
}
|
||||
|
||||
export async function exportDatasetFromPrismaFor(input: { teamId: string; userId: string }) {
|
||||
const root = datasetRoot(input);
|
||||
const tmp = root + ".tmp";
|
||||
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
await ensureDir(tmp);
|
||||
|
||||
const contactsDir = path.join(tmp, "contacts");
|
||||
const notesDir = path.join(tmp, "notes");
|
||||
const messagesDir = path.join(tmp, "messages");
|
||||
const eventsDir = path.join(tmp, "events");
|
||||
const indexDir = path.join(tmp, "index");
|
||||
await Promise.all([
|
||||
ensureDir(contactsDir),
|
||||
ensureDir(notesDir),
|
||||
ensureDir(messagesDir),
|
||||
ensureDir(eventsDir),
|
||||
ensureDir(indexDir),
|
||||
]);
|
||||
|
||||
const contacts = await prisma.contact.findMany({
|
||||
where: { teamId: input.teamId },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
include: {
|
||||
note: { select: { content: true, updatedAt: true } },
|
||||
messages: {
|
||||
select: { direction: true, channel: true, content: true, occurredAt: true },
|
||||
orderBy: { occurredAt: "asc" },
|
||||
},
|
||||
events: {
|
||||
select: { title: true, startsAt: true, endsAt: true, status: true, note: true },
|
||||
orderBy: { startsAt: "asc" },
|
||||
},
|
||||
},
|
||||
take: 5000,
|
||||
});
|
||||
|
||||
const contactIndex = [];
|
||||
|
||||
for (const c of contacts) {
|
||||
const contactFile = path.join(contactsDir, `${c.id}.json`);
|
||||
await writeJson(contactFile, {
|
||||
id: c.id,
|
||||
teamId: c.teamId,
|
||||
name: c.name,
|
||||
company: c.company ?? null,
|
||||
email: c.email ?? null,
|
||||
phone: c.phone ?? null,
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
});
|
||||
|
||||
const noteFile = path.join(notesDir, `${c.id}.md`);
|
||||
await fs.writeFile(
|
||||
noteFile,
|
||||
(c.note?.content?.trim() ? c.note.content.trim() : "") + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const msgFile = path.join(messagesDir, `${c.id}.jsonl`);
|
||||
const msgLines = c.messages.map((m) =>
|
||||
jsonlLine({
|
||||
direction: m.direction,
|
||||
channel: m.channel,
|
||||
occurredAt: m.occurredAt,
|
||||
content: m.content,
|
||||
}),
|
||||
);
|
||||
await fs.writeFile(msgFile, msgLines.join(""), "utf8");
|
||||
|
||||
const evFile = path.join(eventsDir, `${c.id}.jsonl`);
|
||||
const evLines = c.events.map((e) =>
|
||||
jsonlLine({
|
||||
title: e.title,
|
||||
startsAt: e.startsAt,
|
||||
endsAt: e.endsAt,
|
||||
status: e.status ?? null,
|
||||
note: e.note ?? null,
|
||||
}),
|
||||
);
|
||||
await fs.writeFile(evFile, evLines.join(""), "utf8");
|
||||
|
||||
const lastMessageAt = c.messages.length ? c.messages[c.messages.length - 1].occurredAt : null;
|
||||
const nextEventAt = c.events.find((e) => new Date(e.startsAt).getTime() >= Date.now())?.startsAt ?? null;
|
||||
|
||||
contactIndex.push({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
company: c.company ?? null,
|
||||
lastMessageAt,
|
||||
nextEventAt,
|
||||
updatedAt: c.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
await writeJson(path.join(indexDir, "contacts.json"), contactIndex);
|
||||
|
||||
const meta: ExportMeta = { exportedAt: new Date().toISOString(), version: 1 };
|
||||
await writeJson(path.join(tmp, "meta.json"), meta);
|
||||
|
||||
await ensureDir(path.dirname(root));
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
await fs.rename(tmp, root);
|
||||
}
|
||||
|
||||
export async function ensureDataset(input: { teamId: string; userId: string }) {
|
||||
const root = datasetRoot(input);
|
||||
try {
|
||||
const metaPath = path.join(root, "meta.json");
|
||||
await fs.access(metaPath);
|
||||
return;
|
||||
} catch {
|
||||
// fallthrough
|
||||
}
|
||||
await exportDatasetFromPrismaFor(input);
|
||||
}
|
||||
6
Frontend/server/dataset/paths.ts
Normal file
6
Frontend/server/dataset/paths.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import path from "node:path";
|
||||
|
||||
export function datasetRoot(input: { teamId: string; userId: string }) {
|
||||
// Keep it outside Frontend so it can be easily ignored and shared.
|
||||
return path.resolve(process.cwd(), "..", ".data", "crmfs", "teams", input.teamId, "users", input.userId);
|
||||
}
|
||||
57
Frontend/server/utils/auth.ts
Normal file
57
Frontend/server/utils/auth.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { prisma } from "./prisma";
|
||||
import type { H3Event } from "h3";
|
||||
|
||||
export type AuthContext = {
|
||||
teamId: string;
|
||||
userId: string;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
// Minimal temporary auth: pick from headers or auto-provision a default team/user.
|
||||
export async function getAuthContext(event: H3Event): Promise<AuthContext> {
|
||||
const hdrTeam = getHeader(event, "x-team-id")?.trim();
|
||||
const hdrUser = getHeader(event, "x-user-id")?.trim();
|
||||
const hdrConv = getHeader(event, "x-conversation-id")?.trim();
|
||||
|
||||
// Ensure default team/user exist.
|
||||
const user =
|
||||
(hdrUser ? await prisma.user.findUnique({ where: { id: hdrUser } }) : null) ??
|
||||
(await prisma.user.upsert({
|
||||
where: { id: "demo-user" },
|
||||
update: { email: "demo@clientsflow.local", name: "Demo User" },
|
||||
create: { id: "demo-user", email: "demo@clientsflow.local", name: "Demo User" },
|
||||
}));
|
||||
|
||||
const team =
|
||||
(hdrTeam
|
||||
? await prisma.team.findUnique({ where: { id: hdrTeam } })
|
||||
: null) ??
|
||||
(await prisma.team.upsert({
|
||||
where: { id: "demo-team" },
|
||||
update: { name: "Demo Team" },
|
||||
create: { id: "demo-team", name: "Demo Team" },
|
||||
}));
|
||||
|
||||
await prisma.teamMember.upsert({
|
||||
where: { teamId_userId: { teamId: team.id, userId: user.id } },
|
||||
update: {},
|
||||
create: { teamId: team.id, userId: user.id, role: "OWNER" },
|
||||
});
|
||||
|
||||
const conversation =
|
||||
(hdrConv
|
||||
? await prisma.chatConversation.findUnique({ where: { id: hdrConv } })
|
||||
: null) ??
|
||||
(await prisma.chatConversation.upsert({
|
||||
where: { id: `pilot-${team.id}` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `pilot-${team.id}`,
|
||||
teamId: team.id,
|
||||
createdByUserId: user.id,
|
||||
title: "Pilot",
|
||||
},
|
||||
}));
|
||||
|
||||
return { teamId: team.id, userId: user.id, conversationId: conversation.id };
|
||||
}
|
||||
17
Frontend/server/utils/prisma.ts
Normal file
17
Frontend/server/utils/prisma.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
export const prisma =
|
||||
globalThis.__prisma ??
|
||||
new PrismaClient({
|
||||
log: ["error", "warn"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalThis.__prisma = prisma;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user