Files
cog-socket/src/routes/experiment/[id]/+page.svelte
2025-07-14 00:24:44 +02:00

262 lines
8.2 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { goto } from '$app/navigation';
import { get } from 'svelte/store';
import FileBrowser from './FileBrowser.svelte';
let experimentId = '';
let experiment: any = null;
let name = '';
let description = '';
let error = '';
let success = '';
let files: any[] = [];
let uploading = false;
let uploadError = '';
let copied = false;
let origin = '';
async function fetchFiles() {
if (!experimentId) return;
console.log('fetchFiles called for experimentId:', experimentId);
const res = await fetch(`/api/experiment/${experimentId}/files`);
if (res.ok) {
const data = await res.json();
console.log('fetchFiles response:', data);
files = data.files;
} else {
console.log('fetchFiles failed:', res.status);
}
}
async function handleFileInput(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files) await uploadFiles(input.files);
}
async function uploadFiles(fileList: FileList) {
uploading = true;
uploadError = '';
try {
for (const file of Array.from(fileList)) {
const form = new FormData();
form.append('file', file);
// Use webkitRelativePath if present, else fallback to file.name
form.append('relativePath', file.webkitRelativePath || file.name);
const res = await fetch(`/api/experiment/${experimentId}/files`, {
method: 'POST',
body: form,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Upload failed');
}
}
await fetchFiles();
} catch (e: any) {
uploadError = e.message || 'Upload failed';
} finally {
uploading = false;
}
}
async function deleteFile(key: string) {
const url = `/api/experiment/${experimentId}/files?key=${encodeURIComponent(key)}`;
const res = await fetch(url, { method: 'DELETE' });
if (res.ok) await fetchFiles();
}
// Helper: Convert flat file list to tree
function buildFileTree(files: any[]): any {
const root: any = {};
for (const file of files) {
const parts = file.name.split('/');
let node = root;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!part) continue;
if (i === parts.length - 1) {
// File
node[part] = { ...file, isFile: true };
} else {
// Folder
node[part] = node[part] || { children: {}, isFile: false };
node = node[part].children;
}
}
}
return root;
}
let expanded = new Set<string>();
function toggleFolder(path: string) {
if (expanded.has(path)) expanded.delete(path);
else expanded.add(path);
expanded = new Set(expanded); // trigger reactivity
}
function copyLink() {
const link = `${origin}/public/run/${experiment.id}`;
navigator.clipboard.writeText(link);
copied = true;
setTimeout(() => (copied = false), 2000);
}
async function deleteFileOrFolder(key: string, isFolder: boolean) {
console.log('deleteFileOrFolder called:', { key, isFolder, experimentId });
// If folder, send a delete request with a prefix param
if (isFolder) {
const url = `/api/experiment/${experimentId}/files?prefix=${encodeURIComponent(key)}`;
console.log('Deleting folder with URL:', url);
const res = await fetch(url, { method: 'DELETE' });
console.log('Folder delete response:', res.status, res.ok);
if (res.ok) await fetchFiles();
} else {
console.log('Deleting file with key:', key);
await deleteFile(key);
}
}
onMount(async () => {
origin = window.location.origin;
const params = get(page).params;
experimentId = params.id;
const res = await fetch(`/api/experiment?id=${experimentId}`);
if (res.ok) {
const data = await res.json();
experiment = data.experiment;
console.log(experiment)
name = experiment.name;
description = experiment.description;
} else {
error = 'Failed to load experiment.';
}
await fetchFiles();
});
async function handleSave() {
error = '';
success = '';
const res = await fetch(`/api/experiment?id=${experimentId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
});
if (res.ok) {
success = 'Experiment updated!';
} else {
error = 'Failed to update experiment.';
}
}
</script>
{#if experiment}
<div class="flex flex-col min-h-screen bg-background py-8">
<Card class="mb-8 w-full max-w-xl mx-auto">
<CardHeader>
<CardTitle>Experiment Details</CardTitle>
</CardHeader>
<CardContent>
<div class="mb-4">
<Label>ID</Label>
<div class="text-sm text-muted-foreground mb-2">{experiment.id}</div>
</div>
<form on:submit|preventDefault={handleSave} class="space-y-4">
<div>
<Label for="name">Name</Label>
<Input id="name" type="text" bind:value={name} required />
</div>
<div>
<Label for="description">Description</Label>
<Input id="description" type="text" bind:value={description} />
</div>
<div>
<Label>Created At</Label>
<div class="text-sm text-muted-foreground mb-2">{new Date(experiment.createdAt).toLocaleString()}</div>
</div>
{#if error}
<div class="text-red-500 text-sm">{error}</div>
{/if}
{#if success}
<div class="text-green-600 text-sm">{success}</div>
{/if}
<Button type="submit">Save Changes</Button>
</form>
</CardContent>
</Card>
<Card class="mb-8 w-full max-w-xl mx-auto">
<CardHeader>
<CardTitle>Public Link</CardTitle>
</CardHeader>
<CardContent>
<p class="text-sm text-muted-foreground mb-2">
Share this link with your participants to run the experiment.
</p>
<div class="flex items-center space-x-2">
<Input id="public-link" type="text" readonly value="{`${origin}/public/run/${experiment.id}`}" />
<Button on:click={copyLink}>Copy</Button>
</div>
{#if copied}
<p class="text-green-600 text-sm mt-2">Copied to clipboard!</p>
{/if}
</CardContent>
</Card>
<Card class="w-full max-w-xl mx-auto">
<CardHeader>
<CardTitle>Experiment Files</CardTitle>
</CardHeader>
<CardContent>
<div class="mt-8">
<Label>Experiment Files</Label>
<div
class="border-2 border-dashed rounded-md p-4 mb-4 text-center cursor-pointer bg-muted hover:bg-accent transition"
on:click={() => document.getElementById('file-input')?.click()}
>
<div>Click to select files or a folder to upload (folder structure will be preserved)</div>
<input id="file-input" type="file" multiple webkitdirectory class="hidden" on:change={handleFileInput} />
</div>
{#if uploading}
<div class="text-blue-600 text-sm mb-2">Uploading...</div>
{/if}
{#if uploadError}
<div class="text-red-500 text-sm mb-2">{uploadError}</div>
{/if}
<ul class="divide-y divide-border">
<FileBrowser
tree={buildFileTree(files)}
parentPath=""
{expanded}
onToggle={toggleFolder}
onDelete={deleteFileOrFolder}
/>
{#if files.length === 0}
<li class="text-muted-foreground text-sm py-2">No files uploaded yet.</li>
{/if}
</ul>
</div>
</CardContent>
</Card>
</div>
{:else if error}
<div class="flex justify-center items-center min-h-screen bg-background">
<Card class="w-full max-w-md">
<CardHeader>
<CardTitle>Error</CardTitle>
</CardHeader>
<CardContent>
<div class="text-red-500">{error}</div>
</CardContent>
</Card>
</div>
{:else}
<div class="flex justify-center items-center min-h-screen bg-background">
<div>Loading...</div>
</div>
{/if}