Files
shop/frontend/admin/src/components/AIChatBox.vue
Marek Lenczewski e3e88cc58e wahnsinn vibe
2026-04-16 19:42:06 +02:00

138 lines
4.1 KiB
Vue

<script setup lang="ts">
import type { ProposalCard as ProposalCardType } from "@shop/shared/types";
import { computed, ref } from "vue";
import { api } from "../api";
import ProposalCard from "./ProposalCard.vue";
interface CardState {
card: ProposalCardType;
state: "pending" | "confirmed" | "rejected" | "executing" | "success" | "error";
result?: any;
error?: string;
}
const prompt = ref("");
const planning = ref(false);
const planError = ref("");
const cards = ref<CardState[]>([]);
async function plan() {
if (!prompt.value.trim()) return;
planning.value = true;
planError.value = "";
try {
const r = await api.post("/api/ai_admin/plan", { prompt: prompt.value });
cards.value = (r.data.cards || []).map((c: ProposalCardType) => ({ card: c, state: "pending" }));
if (!cards.value.length) planError.value = "Die KI konnte keinen Aktionsplan erzeugen.";
} catch (e: any) {
planError.value = e.response?.data?.detail || e.message;
} finally {
planning.value = false;
}
}
function updateArgs(i: number, args: Record<string, any>) {
cards.value[i].card.args = args;
// remove from 'missing' those that now have values
cards.value[i].card.missing = (cards.value[i].card.missing || []).filter(
(k) => args[k] === undefined || args[k] === null || args[k] === ""
);
}
async function execute(i: number) {
const cs = cards.value[i];
if (cs.card.missing.length) {
cs.state = "error";
cs.error = `Fehlende Felder: ${cs.card.missing.join(", ")}`;
return;
}
cs.state = "executing";
try {
const r = await api.post("/api/ai_admin/execute", {
cards: [{ tool: cs.card.tool, args: cs.card.args }],
});
const res = r.data.results[0];
if (res.ok) {
cs.state = "success";
cs.result = res.result;
} else {
cs.state = "error";
cs.error = res.error;
}
} catch (e: any) {
cs.state = "error";
cs.error = e.response?.data?.detail || e.message;
}
}
function reject(i: number) {
cards.value[i].state = "rejected";
}
async function confirmAll() {
for (let i = 0; i < cards.value.length; i++) {
if (cards.value[i].state === "pending") {
await execute(i);
}
}
}
function reset() {
cards.value = [];
prompt.value = "";
planError.value = "";
}
const pendingCount = computed(() => cards.value.filter((c) => c.state === "pending").length);
</script>
<template>
<div class="card">
<h2 class="text-lg font-semibold mb-2">🤖 KI-Assistent</h2>
<p class="text-sm text-gray-600 mb-3">
Sag, was getan werden soll. Die KI erzeugt nur Vorschläge ausgeführt wird erst nach
deiner Bestätigung. Du kannst auch JSON-Daten reinwerfen.
</p>
<div class="flex gap-2">
<textarea
v-model="prompt"
rows="3"
class="input font-mono text-sm"
placeholder="z.B. 'setze den Shopnamen auf TEST123' oder [{sku:'NEW-1',...}] erstelle diese"
/>
<div class="flex flex-col gap-2">
<button @click="plan" :disabled="planning" class="btn-primary">
{{ planning ? "Plane..." : "Planen" }}
</button>
<button v-if="cards.length" @click="reset" class="btn-secondary">Reset</button>
</div>
</div>
<div v-if="planning" class="text-xs text-gray-500 mt-2">
Lokales LLM rechnet. Bei Bulk-Operationen über viele Items kann das ein paar Minuten dauern.
</div>
<div v-if="planError" class="text-red-600 text-sm mt-2">{{ planError }}</div>
<div v-if="cards.length" class="mt-4">
<div class="flex items-center justify-between mb-3">
<div class="font-medium">{{ cards.length }} Vorschlag(e)</div>
<button v-if="pendingCount > 1" @click="confirmAll" class="btn-success text-sm">
Alle bestätigen ({{ pendingCount }})
</button>
</div>
<div class="space-y-3">
<ProposalCard
v-for="(cs, i) in cards"
:key="i"
:card="cs.card"
:state="cs.state"
:result="cs.result"
:error="cs.error"
@update:args="(a) => updateArgs(i, a)"
@confirm="execute(i)"
@reject="reject(i)"
/>
</div>
</div>
</div>
</template>