262 lines
8.2 KiB
Svelte
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} |