feat(filelist): added file list ui

Drop down menu that shows it all
This commit is contained in:
JK-le-dev 2025-10-19 07:52:13 -05:00
commit 8917a4d1a5
5 changed files with 127 additions and 87 deletions

View file

@ -6,7 +6,7 @@ export default function DownButton({ onClick }) {
return (
<motion.button
onClick={onClick}
className="bg-gray-700 p-2 rounded-2xl file-input border-2 border-gray-600"
className="bg-gray-700 p-2 rounded-2xl file-input border-2 border-gray-600 size-10"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>

View file

@ -1,71 +1,33 @@
import React, { useState, useRef } from "react";
import { X } from "lucide-react";
import React, { forwardRef, useRef } from "react";
import { motion } from "motion/react";
import { FilePlus2 } from "lucide-react";
export default function SchematicButton({ onFiles }) {
const [filesList, setFilesList] = useState([]);
// Hidden file input that exposes an open() method via ref
const SchematicButton = forwardRef(function SchematicButton({ onFiles }, ref) {
const inputRef = useRef(null);
React.useImperativeHandle(ref, () => ({
open: () => inputRef.current && inputRef.current.click(),
}));
function handleFiles(e) {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
setFilesList((s) => [...s, ...files]);
if (onFiles) onFiles(files);
if (inputRef.current) inputRef.current.value = null;
}
function removeFile(index) {
setFilesList((s) => {
const copy = [...s];
copy.splice(index, 1);
return copy;
});
}
return (
<div className="flex items-center gap-2">
<label className="relative inline-block">
<motion.input
ref={inputRef}
type="file"
accept="image/*,application/pdf"
multiple
onChange={handleFiles}
className="file-input hidden"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
/>
<motion.div
className="bg-gray-700 p-2 rounded-2xl cursor-pointer border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => inputRef.current && inputRef.current.click()}
>
<FilePlus2 />
</motion.div>
</label>
{filesList.length > 0 && (
<div className="flex gap-2 items-center max-w-xs flex-wrap">
{filesList.map((f, i) => (
<div
key={i}
className="flex items-center gap-2 bg-gray-800 text-sm text-slate-200 px-2 py-1 rounded"
>
<span className="truncate max-w-[10rem]">{f.name}</span>
<button
onClick={() => removeFile(i)}
className="bg-gray-900 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs border-2 border-gray-700"
aria-label="Remove file"
>
<X />
</button>
</div>
))}
</div>
)}
</div>
<motion.input
ref={inputRef}
type="file"
accept="image/*,application/pdf"
multiple
onChange={handleFiles}
className="file-input hidden"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
/>
);
}
});
export default SchematicButton;

View file

@ -2,9 +2,9 @@ import React, { useMemo, useState } from "react";
import { motion } from "motion/react";
import { Rocket } from "lucide-react";
import DeleteButton from "src/components/ui/button/delete-button";
import SchematicButton from "../button/schematic-button";
import FileList from "src/components/ui/file/file-list";
export default function ChatHeader({ title = "Title of Chat" }) {
export default function ChatHeader({ title = "Schematic Spelunker" }) {
const isDebug = useMemo(() => {
const p = new URLSearchParams(window.location.search);
return p.get("debug") === "1";
@ -17,7 +17,9 @@ export default function ChatHeader({ title = "Title of Chat" }) {
setIngesting(true);
const res = await fetch("/api/files/import-demo", { method: "POST" });
const json = await res.json().catch(() => ({}));
setToast(`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`);
setToast(
`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`
);
setTimeout(() => setToast(""), 4000);
} catch (e) {
setToast("Import failed");
@ -31,25 +33,23 @@ export default function ChatHeader({ title = "Title of Chat" }) {
<div className="w-full flex justify-center">
<header className="text-slate-100 fixed top-4 max-w-3xl w-full px-4">
<div className="flex justify-between items-center gap-4">
<SchematicButton />
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
{title}
</h1>
<DeleteButton />
{isDebug && (
<motion.button
onClick={triggerDemoIngest}
className="bg-gray-800 border-2 border-gray-700 rounded-xl px-3 py-2 flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
disabled={ingesting}
>
<Rocket size={16} />
{ingesting ? "Seeding…" : "Seed Demo Data"}
</motion.button>
)}
</div>
<FileList />
<h1 className="text-sm lg:text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
{title}
</h1>
<DeleteButton />
{isDebug && (
<motion.button
onClick={triggerDemoIngest}
className="bg-gray-800 border-2 border-gray-700 rounded-xl px-3 py-2 flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
disabled={ingesting}
>
<Rocket size={16} />
{ingesting ? "Seeding…" : "Seed Demo Data"}
</motion.button>
)}
</div>
{toast && (
<div className="mt-2 text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-2 py-1 inline-block">

View file

@ -1,5 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
import DeleteButton from "src/components/ui/button/delete-button";
import DownButton from "src/components/ui/button/down-button";
import { motion } from "motion/react";
import { BotMessageSquare } from "lucide-react";
@ -24,11 +23,7 @@ export default function MessageInput({ onSend }) {
<div className="w-full flex justify-center">
<footer className="fixed bottom-6 max-w-3xl w-full px-4">
<div className="flex flex-col gap-4">
<div className="flex justify-between items-end">
<div className="flex">
<DownButton></DownButton>
</div>
</div>
<DownButton></DownButton>
<form
onSubmit={handleSubmit}
className="bg-gray-900 rounded-2xl border-2 border-gray-800 shadow-lg shadow-indigo-600"

View file

@ -0,0 +1,83 @@
import React, { useRef, useState } from "react";
import SchematicButton from "src/components/ui/button/schematic-button";
import { motion } from "motion/react";
import { Menu } from "lucide-react";
import { X } from "lucide-react";
import { FilePlus2 } from "lucide-react";
export default function FileList() {
const pickerRef = useRef(null);
const [open, setOpen] = useState(false);
const [files, setFiles] = useState([]);
function handleAdd() {
if (pickerRef.current && pickerRef.current.open) pickerRef.current.open();
}
function handleFiles(selected) {
setFiles((s) => [...s, ...selected]);
setOpen(true);
}
function removeFile(i) {
setFiles((s) => s.filter((_, idx) => idx !== i));
}
return (
<div className="h-full flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<motion.button
onClick={() => setOpen((v) => !v)}
className="p-2 rounded-xl bg-gray-700 border-2 border-gray-600"
aria-expanded={open}
whileHover={{ scale: 1.1 }}
whileTab={{ scale: 0.9 }}
>
{open ? <X /> : <Menu />}
</motion.button>
</div>
{open && (
<div className="fixed left-1/2 top-24 transform -translate-x-1/2 z-50 w-full max-w-3xl px-4">
<div className="bg-gray-900 border-2 border-gray-800 rounded-2xl p-4 shadow-lg overflow-auto">
<div className="flex items-center justify-between mb-2 pr-1">
<div className="text-lg font-medium">Files</div>
<div>
<motion.button
onClick={handleAdd}
className="w-full bg-gray-700 text-sm p-2 rounded-full border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<FilePlus2 />
</motion.button>
<SchematicButton ref={pickerRef} onFiles={handleFiles} />
</div>
</div>
<div className="flex flex-col gap-2">
{files.length === 0 ? (
<div className="text-md text-slate-400">No files added</div>
) : (
files.map((f, i) => (
<div
key={i}
className="flex items-center justify-between bg-gray-800 p-2 rounded-lg text-sm"
>
<span className="truncate max-w-[24rem]">{f.name}</span>
<button
onClick={() => removeFile(i)}
className="text-xs bg-gray-700 rounded-full p-2"
>
<X />
</button>
</div>
))
)}
</div>
</div>
</div>
)}
</div>
);
}