AlgoVision / frontend /app /components /PlanReviewPanel.vue
AlgoVision Deployer
deploy: minimal bootloader for public Space
1a25b7f
<template>
<div
class="flex flex-col gap-8 w-full animate-fade-in-up max-h-[78vh] overflow-y-auto pr-1 custom-scrollbar"
>
<!-- Header Section -->
<div
class="premium-glass rounded-3xl p-6 border border-white/20 shadow-xl bg-white/70 backdrop-blur-xl sticky top-0 z-20"
>
<div class="flex items-center justify-between gap-4 flex-wrap">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 shadow-inner"
>
<UIcon
name="i-lucide-scroll-text"
class="w-6 h-6 text-indigo-600"
/>
</div>
<div>
<h2 class="text-xl font-black text-slate-900 leading-tight">
Review Video Plan
</h2>
<p class="text-sm text-slate-500 font-medium mt-0.5">
Iterate on scene details before starting production
</p>
</div>
</div>
<div class="flex items-center gap-3">
<UButton
@click="handleRevise"
:loading="store.isRevising"
:disabled="!hasAnyFeedback || store.isGenerating"
variant="soft"
color="primary"
icon="i-lucide-refresh-cw"
class="rounded-xl px-5 py-2.5 font-bold transition-all hover:scale-105 active:scale-95"
>
Revise Plans
</UButton>
<UButton
@click="handleApprove"
:loading="store.isGenerating && !store.isRevising"
:disabled="store.isRevising"
class="btn-premium rounded-xl px-6 py-2.5 font-black text-white shadow-lg shadow-green-500/20 transition-all hover:scale-105 active:scale-95 flex items-center gap-2"
>
<UIcon name="i-lucide-check-circle" class="w-5 h-5" />
Approve & Generate
</UButton>
</div>
</div>
</div>
<!-- Scene Plans List -->
<div class="flex flex-col gap-6">
<div
v-for="(scene, idx) in project.scenes"
:key="idx"
class="premium-glass rounded-3xl overflow-hidden border border-white/20 shadow-lg group hover:shadow-xl transition-all duration-500"
:class="{ 'opacity-60 grayscale-[0.2]': store.isRevising }"
>
<!-- Scene Header -->
<div
class="bg-white/60 p-5 border-b border-slate-100 flex items-center justify-between gap-4"
>
<div class="flex items-center gap-4">
<div
class="w-9 h-9 rounded-xl bg-slate-900 text-white font-mono text-sm font-black flex items-center justify-center shadow-md"
>
{{ Number(idx) + 1 }}
</div>
<div>
<h3 class="font-black text-slate-900 text-lg">
{{ scene.title || `Scene ${Number(idx) + 1}` }}
</h3>
<p
class="text-xs text-indigo-500 font-bold uppercase tracking-wider"
>
Strategy & Review
</p>
</div>
</div>
</div>
<div class="p-6 grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Current Plan Details (Parsed) -->
<div class="space-y-6">
<div class="space-y-4">
<div
v-if="scene.purpose"
class="bg-blue-50/50 p-3 rounded-lg border border-blue-100"
>
<h5
class="text-[10px] font-bold uppercase tracking-wider text-blue-500 mb-1 flex items-center gap-1"
>
<UIcon name="i-lucide-target" class="w-3 h-3" />
Scene Purpose
</h5>
<p class="text-xs text-slate-700 italic leading-snug">
{{ scene.purpose }}
</p>
</div>
<div v-if="scene.description">
<h5
class="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1"
>
Visual Concept
</h5>
<p class="text-xs text-slate-800 leading-relaxed">
{{ scene.description }}
</p>
</div>
</div>
</div>
<!-- Right: Feedback Section -->
<div class="flex flex-col h-full">
<h4
class="text-[11px] font-black uppercase tracking-widest text-slate-400 mb-2 flex items-center gap-1.5"
>
<UIcon name="i-lucide-message-square" class="w-3.5 h-3.5" />
Your Feedback / Changes
</h4>
<div class="flex-1 relative group/input">
<textarea
v-model="feedbacks[Number(idx) + 1]"
placeholder="What should change in this scene?"
class="w-full h-full min-h-[120px] p-4 rounded-2xl bg-white border-2 border-slate-100 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-sm font-medium placeholder:text-slate-300 resize-none shadow-inner"
></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useProjectStore } from "~/stores/projects";
const props = defineProps<{
project: any;
}>();
const store = useProjectStore();
const feedbacks = ref<Record<number, string>>({});
const hasAnyFeedback = computed(() => {
return Object.values(feedbacks.value).some((f) => f.trim().length > 0);
});
function extractContent(plan: string, tag: string) {
if (!plan) return "No plan details available.";
const regex = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, "i");
const match = plan.match(regex);
return match && match[1] ? match[1].trim() : plan.trim();
}
async function handleRevise() {
const activeFeedbacks: Record<number, string> = {};
for (const [id, msg] of Object.entries(feedbacks.value)) {
if (msg.trim()) activeFeedbacks[Number(id)] = msg.trim();
}
if (Object.keys(activeFeedbacks).length === 0) return;
await store.revisePlan(props.project.id, activeFeedbacks);
// Clear feedbacks after revision
feedbacks.value = {};
}
async function handleApprove() {
await store.approvePlan(props.project.id);
}
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
}
</style>