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 ( return (
<motion.button <motion.button
onClick={onClick} 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 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
> >

View file

@ -1,71 +1,33 @@
import React, { useState, useRef } from "react"; import React, { forwardRef, useRef } from "react";
import { X } from "lucide-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { FilePlus2 } from "lucide-react";
export default function SchematicButton({ onFiles }) { // Hidden file input that exposes an open() method via ref
const [filesList, setFilesList] = useState([]); const SchematicButton = forwardRef(function SchematicButton({ onFiles }, ref) {
const inputRef = useRef(null); const inputRef = useRef(null);
React.useImperativeHandle(ref, () => ({
open: () => inputRef.current && inputRef.current.click(),
}));
function handleFiles(e) { function handleFiles(e) {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
if (files.length === 0) return; if (files.length === 0) return;
setFilesList((s) => [...s, ...files]);
if (onFiles) onFiles(files); if (onFiles) onFiles(files);
if (inputRef.current) inputRef.current.value = null; if (inputRef.current) inputRef.current.value = null;
} }
function removeFile(index) {
setFilesList((s) => {
const copy = [...s];
copy.splice(index, 1);
return copy;
});
}
return ( return (
<div className="flex items-center gap-2"> <motion.input
<label className="relative inline-block"> ref={inputRef}
<motion.input type="file"
ref={inputRef} accept="image/*,application/pdf"
type="file" multiple
accept="image/*,application/pdf" onChange={handleFiles}
multiple className="file-input hidden"
onChange={handleFiles} whileHover={{ scale: 1.02 }}
className="file-input hidden" whileTap={{ scale: 0.98 }}
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>
); );
} });
export default SchematicButton;

View file

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