File size: 8,372 Bytes
fe029e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { useEffect, useState } from "react";
import { Bot, Clock, Film, RefreshCw, Sparkles, UserCircle } from "lucide-react";

interface Project {
  id: string;
  title: string;
  content?: string | null;
  synopsis?: string | null;
  description?: string | null;
  aspect_ratio: string;
  duration_seconds: number | null;
  status: string;
  characters: string[];
  metadata: Record<string, unknown>;
  created_at?: string;
  updated_at?: string;
}

interface ProjectsResponse {
  projects?: Project[];
}

interface ProjectsViewProps {
  compact?: boolean;
  onOpenProject?: (id: string) => void;
}

const STATUS_STYLES: Record<string, string> = {
  draft: "border-gray-700/60 bg-gray-800/40 text-gray-400",
  planning: "border-violet-500/30 bg-violet-500/10 text-violet-300",
  ready: "border-emerald-500/30 bg-emerald-500/10 text-emerald-300",
  rendering: "border-amber-500/30 bg-amber-500/10 text-amber-300",
  completed: "border-blue-500/30 bg-blue-500/10 text-blue-300",
  failed: "border-red-500/30 bg-red-500/10 text-red-300",
};

function formatRelative(iso?: string): string {
  if (!iso) return "—";
  const then = new Date(iso).getTime();
  if (Number.isNaN(then)) return "—";
  const diff = Date.now() - then;
  const mins = Math.round(diff / 60000);
  if (mins < 1) return "just now";
  if (mins < 60) return `${mins}m ago`;
  const hours = Math.round(mins / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.round(hours / 24);
  return `${days}d ago`;
}

function projectText(project: Project): string | null {
  return project.synopsis || project.content || project.description || null;
}

export function ProjectsView({ compact = false, onOpenProject }: ProjectsViewProps) {
  const [projects, setProjects] = useState<Project[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  async function load() {
    setError(null);
    try {
      const response = await fetch("/api/projects");
      if (!response.ok) throw new Error(`/api/projects returned ${response.status}`);
      const data = await response.json() as ProjectsResponse;
      setProjects(data.projects || []);
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to load projects");
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    load();
  }, []);

  return (
    <div className={compact ? "h-full overflow-y-auto p-4 space-y-5" : "h-full overflow-y-auto p-8 space-y-6"}>
      <section className="space-y-2">
        <div className="flex items-center gap-3">
          <div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-violet-500 to-indigo-500 flex items-center justify-center shadow-lg shadow-violet-500/20 ring-1 ring-white/10">
            <Film className="w-5 h-5 text-white" />
          </div>
          <div className="min-w-0">
            <h2 className="text-sm font-bold tracking-tight text-gray-100">Projects</h2>
            <p className="text-xs text-gray-500 leading-relaxed">Multi-shot stories your agent is directing.</p>
          </div>
          <button
            onClick={() => { setLoading(true); load(); }}
            className="ml-auto w-8 h-8 rounded-xl border border-gray-800 bg-gray-900/40 text-gray-500 hover:text-gray-200 hover:border-gray-600 transition flex items-center justify-center"
            title="Refresh projects"
          >
            <RefreshCw className={`w-3.5 h-3.5 ${loading ? "animate-spin" : ""}`} />
          </button>
        </div>
      </section>

      <section className="rounded-2xl border border-rose-600/20 bg-gradient-to-b from-rose-950/10 to-gray-950 p-4 space-y-3">
        <div className="flex items-center gap-2">
          <Sparkles className="w-4 h-4 text-rose-300" />
          <p className="text-xs font-semibold text-rose-200 uppercase tracking-wider">What this tab is for</p>
        </div>
        <p className="text-xs text-gray-300 leading-relaxed">
          This is not raw generation. This is where a rough idea becomes a structured short: synopsis, scenes, shots, and image prompts.
        </p>
        <div className="rounded-xl border border-gray-800/60 bg-black/30 p-3 space-y-1.5">
          <p className="text-[10px] uppercase tracking-wider text-gray-500">Tell your agent</p>
          <p className="text-[11px] text-gray-300 italic leading-relaxed">“Put me inside an Iron Man-style short. Workshop, helmet reveal, first flight. Around 30 seconds.”</p>
          <p className="text-[11px] text-gray-300 italic leading-relaxed">“Make a dark cyberpunk teaser with my character walking through neon rain.”</p>
        </div>
      </section>

      <section className="rounded-2xl border border-violet-600/20 bg-gradient-to-b from-violet-950/10 to-gray-950 p-4 space-y-3">
        <div className="flex items-center gap-2">
          <Bot className="w-4 h-4 text-violet-300" />
          <p className="text-xs font-semibold text-violet-200 uppercase tracking-wider">Agent workflow</p>
        </div>
        <ol className="text-[11px] text-gray-400 space-y-1.5 leading-relaxed list-decimal pl-4">
          <li>Agent writes the project outline in conversation.</li>
          <li>Agent creates project, scenes, and shots through the API.</li>
          <li>User reviews the outline here before rendering.</li>
          <li>Agent generates storyboard images shot by shot.</li>
          <li>Approved images are animated into video clips.</li>
        </ol>
      </section>

      <section className="space-y-3">
        <div className="flex items-center justify-between">
          <p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Existing projects</p>
          <span className="text-[10px] text-gray-600">{projects.length}</span>
        </div>

        {error && (
          <div className="rounded-xl border border-red-500/30 bg-red-950/20 px-3 py-2 text-xs text-red-300">
            {error}
          </div>
        )}

        {!loading && !error && projects.length === 0 && (
          <div className="rounded-xl border border-gray-800/60 bg-gray-900/30 p-5 text-center">
            <UserCircle className="w-5 h-5 text-gray-600 mx-auto mb-2" />
            <p className="text-xs text-gray-400">No projects yet.</p>
            <p className="text-[11px] text-gray-600 mt-1 leading-relaxed">Ask your agent to draft one, then it will appear here.</p>
          </div>
        )}

        {projects.map((project) => {
          const statusStyle = STATUS_STYLES[project.status] || STATUS_STYLES.draft;
          const text = projectText(project);
          return (
            <article
              key={project.id}
              onClick={() => onOpenProject?.(project.id)}
              className={`rounded-xl border border-gray-800/60 bg-gray-900/30 p-3 space-y-2 ${onOpenProject ? "cursor-pointer hover:bg-gray-900/50 hover:border-gray-700 transition" : ""}`}
            >
              <div className="flex items-start justify-between gap-2">
                <div className="min-w-0">
                  <h3 className="text-xs font-semibold text-gray-100 truncate">{project.title}</h3>
                  {text && <p className="text-[11px] text-gray-500 mt-1 line-clamp-2 leading-relaxed">{text}</p>}
                </div>
                <span className={`flex-shrink-0 text-[9px] uppercase tracking-wider rounded-full border px-1.5 py-0.5 ${statusStyle}`}>
                  {project.status}
                </span>
              </div>
              <div className="flex items-center gap-3 pt-2 border-t border-gray-800/40 text-[10px] text-gray-600">
                <span className="font-mono">{project.aspect_ratio}</span>
                {project.duration_seconds !== null && (
                  <span className="flex items-center gap-1"><Clock className="w-3 h-3" />{project.duration_seconds}s</span>
                )}
                <span className="ml-auto">{formatRelative(project.updated_at)}</span>
              </div>
            </article>
          );
        })}

        {loading && projects.length === 0 && !error && (
          <div className="text-center py-8">
            <RefreshCw className="w-5 h-5 text-gray-600 mx-auto animate-spin" />
            <p className="text-xs text-gray-600 mt-2">Loading projects…</p>
          </div>
        )}
      </section>
    </div>
  );
}