Supabase backend + UI dropdown updates
Browse files- src/app/api/admin/requests/[type]/[id]/comments/route.ts +2 -2
- src/app/api/admin/requests/[type]/[id]/route.ts +2 -2
- src/app/api/dataset/route.ts +3 -3
- src/app/api/distillation/route.ts +3 -3
- src/app/api/requests/[type]/[id]/comments/[commentId]/route.ts +4 -4
- src/app/api/requests/[type]/[id]/comments/route.ts +2 -2
- src/app/api/requests/[type]/[id]/route.ts +7 -7
- src/app/page.tsx +68 -10
- src/components/ui/combobox.tsx +24 -4
- src/lib/store.ts +348 -279
src/app/api/admin/requests/[type]/[id]/comments/route.ts
CHANGED
|
@@ -19,7 +19,7 @@ export async function POST(
|
|
| 19 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 20 |
}
|
| 21 |
|
| 22 |
-
const existing = getRequest(type, params.id);
|
| 23 |
if (!existing) {
|
| 24 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 25 |
}
|
|
@@ -30,7 +30,7 @@ export async function POST(
|
|
| 30 |
return NextResponse.json({ error: "Comment body is required" }, { status: 400 });
|
| 31 |
}
|
| 32 |
|
| 33 |
-
const comment = addAdminComment(type, params.id, text, userId);
|
| 34 |
const response = NextResponse.json({ ok: true, comment });
|
| 35 |
if (shouldSetCookie) {
|
| 36 |
response.cookies.set({ ...userCookieOptions(), value: userId });
|
|
|
|
| 19 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 20 |
}
|
| 21 |
|
| 22 |
+
const existing = await getRequest(type, params.id);
|
| 23 |
if (!existing) {
|
| 24 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 25 |
}
|
|
|
|
| 30 |
return NextResponse.json({ error: "Comment body is required" }, { status: 400 });
|
| 31 |
}
|
| 32 |
|
| 33 |
+
const comment = await addAdminComment(type, params.id, text, userId);
|
| 34 |
const response = NextResponse.json({ ok: true, comment });
|
| 35 |
if (shouldSetCookie) {
|
| 36 |
response.cookies.set({ ...userCookieOptions(), value: userId });
|
src/app/api/admin/requests/[type]/[id]/route.ts
CHANGED
|
@@ -23,7 +23,7 @@ export async function PATCH(
|
|
| 23 |
return NextResponse.json({ error: "Invalid status" }, { status: 400 });
|
| 24 |
}
|
| 25 |
|
| 26 |
-
const ok = updateRequestStatus(type, params.id, status);
|
| 27 |
if (!ok) {
|
| 28 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 29 |
}
|
|
@@ -50,7 +50,7 @@ export async function DELETE(
|
|
| 50 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 51 |
}
|
| 52 |
|
| 53 |
-
const ok = deleteRequest(type, params.id);
|
| 54 |
if (!ok) {
|
| 55 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 56 |
}
|
|
|
|
| 23 |
return NextResponse.json({ error: "Invalid status" }, { status: 400 });
|
| 24 |
}
|
| 25 |
|
| 26 |
+
const ok = await updateRequestStatus(type, params.id, status);
|
| 27 |
if (!ok) {
|
| 28 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 29 |
}
|
|
|
|
| 50 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 51 |
}
|
| 52 |
|
| 53 |
+
const ok = await deleteRequest(type, params.id);
|
| 54 |
if (!ok) {
|
| 55 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 56 |
}
|
src/app/api/dataset/route.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { getOrCreateUserId, userCookieOptions } from "@/lib/userIdentity";
|
|
| 8 |
|
| 9 |
export async function GET() {
|
| 10 |
try {
|
| 11 |
-
const requests = getDatasetRequests();
|
| 12 |
return NextResponse.json(requests);
|
| 13 |
} catch (error) {
|
| 14 |
console.error("Error fetching dataset requests:", error);
|
|
@@ -29,7 +29,7 @@ export async function POST(request: NextRequest) {
|
|
| 29 |
);
|
| 30 |
}
|
| 31 |
|
| 32 |
-
const newRequest = addDatasetRequest({
|
| 33 |
sourceModel,
|
| 34 |
datasetSize: datasetSize || "250x",
|
| 35 |
reasoningDepth: reasoningDepth || "high",
|
|
@@ -62,7 +62,7 @@ export async function PATCH(request: NextRequest) {
|
|
| 62 |
request.headers.get("x-real-ip") ||
|
| 63 |
"anonymous";
|
| 64 |
|
| 65 |
-
const result = upvoteDataset(id, ip);
|
| 66 |
|
| 67 |
if (!result.success) {
|
| 68 |
return NextResponse.json(
|
|
|
|
| 8 |
|
| 9 |
export async function GET() {
|
| 10 |
try {
|
| 11 |
+
const requests = await getDatasetRequests();
|
| 12 |
return NextResponse.json(requests);
|
| 13 |
} catch (error) {
|
| 14 |
console.error("Error fetching dataset requests:", error);
|
|
|
|
| 29 |
);
|
| 30 |
}
|
| 31 |
|
| 32 |
+
const newRequest = await addDatasetRequest({
|
| 33 |
sourceModel,
|
| 34 |
datasetSize: datasetSize || "250x",
|
| 35 |
reasoningDepth: reasoningDepth || "high",
|
|
|
|
| 62 |
request.headers.get("x-real-ip") ||
|
| 63 |
"anonymous";
|
| 64 |
|
| 65 |
+
const result = await upvoteDataset(id, ip);
|
| 66 |
|
| 67 |
if (!result.success) {
|
| 68 |
return NextResponse.json(
|
src/app/api/distillation/route.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { getOrCreateUserId, userCookieOptions } from "@/lib/userIdentity";
|
|
| 8 |
|
| 9 |
export async function GET() {
|
| 10 |
try {
|
| 11 |
-
const requests = getDistillationRequests();
|
| 12 |
return NextResponse.json(requests);
|
| 13 |
} catch (error) {
|
| 14 |
console.error("Error fetching distillation requests:", error);
|
|
@@ -29,7 +29,7 @@ export async function POST(request: NextRequest) {
|
|
| 29 |
);
|
| 30 |
}
|
| 31 |
|
| 32 |
-
const newRequest = addDistillationRequest({
|
| 33 |
sourceDataset,
|
| 34 |
studentModel,
|
| 35 |
additionalNotes: additionalNotes || "",
|
|
@@ -60,7 +60,7 @@ export async function PATCH(request: NextRequest) {
|
|
| 60 |
request.headers.get("x-real-ip") ||
|
| 61 |
"anonymous";
|
| 62 |
|
| 63 |
-
const result = upvoteDistillation(id, ip);
|
| 64 |
|
| 65 |
if (!result.success) {
|
| 66 |
return NextResponse.json(
|
|
|
|
| 8 |
|
| 9 |
export async function GET() {
|
| 10 |
try {
|
| 11 |
+
const requests = await getDistillationRequests();
|
| 12 |
return NextResponse.json(requests);
|
| 13 |
} catch (error) {
|
| 14 |
console.error("Error fetching distillation requests:", error);
|
|
|
|
| 29 |
);
|
| 30 |
}
|
| 31 |
|
| 32 |
+
const newRequest = await addDistillationRequest({
|
| 33 |
sourceDataset,
|
| 34 |
studentModel,
|
| 35 |
additionalNotes: additionalNotes || "",
|
|
|
|
| 60 |
request.headers.get("x-real-ip") ||
|
| 61 |
"anonymous";
|
| 62 |
|
| 63 |
+
const result = await upvoteDistillation(id, ip);
|
| 64 |
|
| 65 |
if (!result.success) {
|
| 66 |
return NextResponse.json(
|
src/app/api/requests/[type]/[id]/comments/[commentId]/route.ts
CHANGED
|
@@ -22,7 +22,7 @@ export async function PATCH(
|
|
| 22 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 23 |
}
|
| 24 |
|
| 25 |
-
const thread = getThread(type, params.id);
|
| 26 |
const comment = thread.comments.find((c) => c.id === params.commentId);
|
| 27 |
if (!comment) {
|
| 28 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
|
@@ -40,7 +40,7 @@ export async function PATCH(
|
|
| 40 |
return NextResponse.json({ error: "Comment body is required" }, { status: 400 });
|
| 41 |
}
|
| 42 |
|
| 43 |
-
const updated = updateComment(type, params.id, params.commentId, text);
|
| 44 |
if (!updated) {
|
| 45 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
| 46 |
}
|
|
@@ -75,7 +75,7 @@ export async function DELETE(
|
|
| 75 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 76 |
}
|
| 77 |
|
| 78 |
-
const thread = getThread(type, params.id);
|
| 79 |
const comment = thread.comments.find((c) => c.id === params.commentId);
|
| 80 |
if (!comment) {
|
| 81 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
|
@@ -89,7 +89,7 @@ export async function DELETE(
|
|
| 89 |
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
| 90 |
}
|
| 91 |
|
| 92 |
-
const ok = deleteComment(type, params.id, params.commentId);
|
| 93 |
if (!ok) {
|
| 94 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
| 95 |
}
|
|
|
|
| 22 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 23 |
}
|
| 24 |
|
| 25 |
+
const thread = await getThread(type, params.id);
|
| 26 |
const comment = thread.comments.find((c) => c.id === params.commentId);
|
| 27 |
if (!comment) {
|
| 28 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
|
|
|
| 40 |
return NextResponse.json({ error: "Comment body is required" }, { status: 400 });
|
| 41 |
}
|
| 42 |
|
| 43 |
+
const updated = await updateComment(type, params.id, params.commentId, text);
|
| 44 |
if (!updated) {
|
| 45 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
| 46 |
}
|
|
|
|
| 75 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 76 |
}
|
| 77 |
|
| 78 |
+
const thread = await getThread(type, params.id);
|
| 79 |
const comment = thread.comments.find((c) => c.id === params.commentId);
|
| 80 |
if (!comment) {
|
| 81 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
|
|
|
| 89 |
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
| 90 |
}
|
| 91 |
|
| 92 |
+
const ok = await deleteComment(type, params.id, params.commentId);
|
| 93 |
if (!ok) {
|
| 94 |
return NextResponse.json({ error: "Comment not found" }, { status: 404 });
|
| 95 |
}
|
src/app/api/requests/[type]/[id]/comments/route.ts
CHANGED
|
@@ -20,7 +20,7 @@ export async function POST(
|
|
| 20 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 21 |
}
|
| 22 |
|
| 23 |
-
const existing = getRequest(type, params.id);
|
| 24 |
if (!existing) {
|
| 25 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 26 |
}
|
|
@@ -33,7 +33,7 @@ export async function POST(
|
|
| 33 |
return NextResponse.json({ error: "Comment body is required" }, { status: 400 });
|
| 34 |
}
|
| 35 |
|
| 36 |
-
const comment = addUserComment(type, params.id, text, author, userId);
|
| 37 |
const response = NextResponse.json({ ok: true, comment });
|
| 38 |
if (shouldSetCookie) {
|
| 39 |
response.cookies.set({ ...userCookieOptions(), value: userId });
|
|
|
|
| 20 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 21 |
}
|
| 22 |
|
| 23 |
+
const existing = await getRequest(type, params.id);
|
| 24 |
if (!existing) {
|
| 25 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 26 |
}
|
|
|
|
| 33 |
return NextResponse.json({ error: "Comment body is required" }, { status: 400 });
|
| 34 |
}
|
| 35 |
|
| 36 |
+
const comment = await addUserComment(type, params.id, text, author, userId);
|
| 37 |
const response = NextResponse.json({ ok: true, comment });
|
| 38 |
if (shouldSetCookie) {
|
| 39 |
response.cookies.set({ ...userCookieOptions(), value: userId });
|
src/app/api/requests/[type]/[id]/route.ts
CHANGED
|
@@ -20,7 +20,7 @@ export async function GET(
|
|
| 20 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 21 |
}
|
| 22 |
|
| 23 |
-
const requestItem = getRequest(type, params.id);
|
| 24 |
if (!requestItem) {
|
| 25 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 26 |
}
|
|
@@ -30,7 +30,7 @@ export async function GET(
|
|
| 30 |
const canEditRequest = admin || (Boolean(ownerId) && ownerId === userId);
|
| 31 |
const canDeleteRequest = canEditRequest;
|
| 32 |
|
| 33 |
-
const thread = getThread(type, params.id);
|
| 34 |
const threadWithPerms = {
|
| 35 |
...thread,
|
| 36 |
comments: thread.comments.map((c) => {
|
|
@@ -72,7 +72,7 @@ export async function PATCH(
|
|
| 72 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 73 |
}
|
| 74 |
|
| 75 |
-
const existing = getRequest(type, params.id);
|
| 76 |
if (!existing) {
|
| 77 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 78 |
}
|
|
@@ -87,12 +87,12 @@ export async function PATCH(
|
|
| 87 |
const body = await request.json();
|
| 88 |
const updated =
|
| 89 |
type === "distillation"
|
| 90 |
-
? updateDistillationRequest(params.id, {
|
| 91 |
sourceDataset: typeof body?.sourceDataset === "string" ? body.sourceDataset : undefined,
|
| 92 |
studentModel: typeof body?.studentModel === "string" ? body.studentModel : undefined,
|
| 93 |
additionalNotes: typeof body?.additionalNotes === "string" ? body.additionalNotes : undefined,
|
| 94 |
})
|
| 95 |
-
: updateDatasetRequest(params.id, {
|
| 96 |
sourceModel: typeof body?.sourceModel === "string" ? body.sourceModel : undefined,
|
| 97 |
datasetSize: typeof body?.datasetSize === "string" ? body.datasetSize : undefined,
|
| 98 |
reasoningDepth: typeof body?.reasoningDepth === "string" ? body.reasoningDepth : undefined,
|
|
@@ -127,7 +127,7 @@ export async function DELETE(
|
|
| 127 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 128 |
}
|
| 129 |
|
| 130 |
-
const existing = getRequest(type, params.id);
|
| 131 |
if (!existing) {
|
| 132 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 133 |
}
|
|
@@ -139,7 +139,7 @@ export async function DELETE(
|
|
| 139 |
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
| 140 |
}
|
| 141 |
|
| 142 |
-
const ok = deleteRequest(type, params.id);
|
| 143 |
if (!ok) {
|
| 144 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 145 |
}
|
|
|
|
| 20 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 21 |
}
|
| 22 |
|
| 23 |
+
const requestItem = await getRequest(type, params.id);
|
| 24 |
if (!requestItem) {
|
| 25 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 26 |
}
|
|
|
|
| 30 |
const canEditRequest = admin || (Boolean(ownerId) && ownerId === userId);
|
| 31 |
const canDeleteRequest = canEditRequest;
|
| 32 |
|
| 33 |
+
const thread = await getThread(type, params.id);
|
| 34 |
const threadWithPerms = {
|
| 35 |
...thread,
|
| 36 |
comments: thread.comments.map((c) => {
|
|
|
|
| 72 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 73 |
}
|
| 74 |
|
| 75 |
+
const existing = await getRequest(type, params.id);
|
| 76 |
if (!existing) {
|
| 77 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 78 |
}
|
|
|
|
| 87 |
const body = await request.json();
|
| 88 |
const updated =
|
| 89 |
type === "distillation"
|
| 90 |
+
? await updateDistillationRequest(params.id, {
|
| 91 |
sourceDataset: typeof body?.sourceDataset === "string" ? body.sourceDataset : undefined,
|
| 92 |
studentModel: typeof body?.studentModel === "string" ? body.studentModel : undefined,
|
| 93 |
additionalNotes: typeof body?.additionalNotes === "string" ? body.additionalNotes : undefined,
|
| 94 |
})
|
| 95 |
+
: await updateDatasetRequest(params.id, {
|
| 96 |
sourceModel: typeof body?.sourceModel === "string" ? body.sourceModel : undefined,
|
| 97 |
datasetSize: typeof body?.datasetSize === "string" ? body.datasetSize : undefined,
|
| 98 |
reasoningDepth: typeof body?.reasoningDepth === "string" ? body.reasoningDepth : undefined,
|
|
|
|
| 127 |
return NextResponse.json({ error: "Invalid request type" }, { status: 400 });
|
| 128 |
}
|
| 129 |
|
| 130 |
+
const existing = await getRequest(type, params.id);
|
| 131 |
if (!existing) {
|
| 132 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 133 |
}
|
|
|
|
| 139 |
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
| 140 |
}
|
| 141 |
|
| 142 |
+
const ok = await deleteRequest(type, params.id);
|
| 143 |
if (!ok) {
|
| 144 |
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
| 145 |
}
|
src/app/page.tsx
CHANGED
|
@@ -53,6 +53,11 @@ interface OpenRouterModel {
|
|
| 53 |
name: string;
|
| 54 |
}
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
const STUDENT_MODELS = [
|
| 57 |
"Qwen3-4B",
|
| 58 |
"Qwen3-8B",
|
|
@@ -64,6 +69,17 @@ const STUDENT_MODELS = [
|
|
| 64 |
"Other",
|
| 65 |
];
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
const REASONING_DEPTHS = ["low", "medium", "high"];
|
| 68 |
const DATASET_SIZES = ["100x", "250x", "500x", "1000x", "3000x", "11000x"];
|
| 69 |
const TOPICS = [
|
|
@@ -94,6 +110,10 @@ export default function Home() {
|
|
| 94 |
const [loadingDatasets, setLoadingDatasets] = useState(false);
|
| 95 |
const [loadingModels, setLoadingModels] = useState(false);
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
// Distillation form state
|
| 98 |
const [sourceDataset, setSourceDataset] = useState("");
|
| 99 |
const [sourceDatasetOther, setSourceDatasetOther] = useState("");
|
|
@@ -115,6 +135,36 @@ export default function Home() {
|
|
| 115 |
fetchOpenrouterModels();
|
| 116 |
}, []);
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
async function fetchRequests() {
|
| 119 |
try {
|
| 120 |
const [distillRes, datasetRes] = await Promise.all([
|
|
@@ -530,16 +580,19 @@ export default function Home() {
|
|
| 530 |
<div className="space-y-2">
|
| 531 |
<Label htmlFor="sourceModel">Source Model *</Label>
|
| 532 |
<Combobox
|
| 533 |
-
options={
|
| 534 |
value={sourceModel}
|
| 535 |
onValueChange={(v) => {
|
| 536 |
setSourceModel(v);
|
| 537 |
-
if (v !== "Other")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
}}
|
| 539 |
-
placeholder="Select
|
| 540 |
searchPlaceholder="Search models..."
|
| 541 |
emptyMessage="No models found"
|
| 542 |
-
loading={loadingModels}
|
| 543 |
/>
|
| 544 |
</div>
|
| 545 |
<div className="space-y-2">
|
|
@@ -560,13 +613,18 @@ export default function Home() {
|
|
| 560 |
</div>
|
| 561 |
{sourceModel === "Other" && (
|
| 562 |
<div className="space-y-2">
|
| 563 |
-
<Label htmlFor="sourceModelOther">
|
| 564 |
-
<
|
| 565 |
-
|
| 566 |
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
| 567 |
value={sourceModelOther}
|
| 568 |
-
|
| 569 |
-
placeholder="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
/>
|
| 571 |
</div>
|
| 572 |
)}
|
|
|
|
| 53 |
name: string;
|
| 54 |
}
|
| 55 |
|
| 56 |
+
interface HuggingFaceModel {
|
| 57 |
+
id: string;
|
| 58 |
+
name: string;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
const STUDENT_MODELS = [
|
| 62 |
"Qwen3-4B",
|
| 63 |
"Qwen3-8B",
|
|
|
|
| 69 |
"Other",
|
| 70 |
];
|
| 71 |
|
| 72 |
+
const SOURCE_MODELS = [
|
| 73 |
+
"google/gemini-3-flash-preview",
|
| 74 |
+
"openai/gpt-4o-mini",
|
| 75 |
+
"openai/gpt-4o",
|
| 76 |
+
"anthropic/claude-3-5-sonnet",
|
| 77 |
+
"deepseek/deepseek-r1",
|
| 78 |
+
"Qwen/Qwen2.5-72B-Instruct",
|
| 79 |
+
"meta-llama/Llama-3.1-8B-Instruct",
|
| 80 |
+
"Other",
|
| 81 |
+
];
|
| 82 |
+
|
| 83 |
const REASONING_DEPTHS = ["low", "medium", "high"];
|
| 84 |
const DATASET_SIZES = ["100x", "250x", "500x", "1000x", "3000x", "11000x"];
|
| 85 |
const TOPICS = [
|
|
|
|
| 110 |
const [loadingDatasets, setLoadingDatasets] = useState(false);
|
| 111 |
const [loadingModels, setLoadingModels] = useState(false);
|
| 112 |
|
| 113 |
+
const [huggingfaceModels, setHuggingfaceModels] = useState<HuggingFaceModel[]>([]);
|
| 114 |
+
const [hfModelQuery, setHfModelQuery] = useState("");
|
| 115 |
+
const [loadingHfModels, setLoadingHfModels] = useState(false);
|
| 116 |
+
|
| 117 |
// Distillation form state
|
| 118 |
const [sourceDataset, setSourceDataset] = useState("");
|
| 119 |
const [sourceDatasetOther, setSourceDatasetOther] = useState("");
|
|
|
|
| 135 |
fetchOpenrouterModels();
|
| 136 |
}, []);
|
| 137 |
|
| 138 |
+
useEffect(() => {
|
| 139 |
+
let cancelled = false;
|
| 140 |
+
const q = hfModelQuery.trim();
|
| 141 |
+
|
| 142 |
+
const timer = setTimeout(async () => {
|
| 143 |
+
setLoadingHfModels(true);
|
| 144 |
+
try {
|
| 145 |
+
const res = await fetch(`/api/huggingface-models?q=${encodeURIComponent(q)}&limit=20`);
|
| 146 |
+
const data = await res.json();
|
| 147 |
+
if (!cancelled) {
|
| 148 |
+
setHuggingfaceModels(Array.isArray(data) ? data : []);
|
| 149 |
+
}
|
| 150 |
+
} catch (error) {
|
| 151 |
+
console.error("Error fetching Hugging Face models:", error);
|
| 152 |
+
if (!cancelled) {
|
| 153 |
+
setHuggingfaceModels([]);
|
| 154 |
+
}
|
| 155 |
+
} finally {
|
| 156 |
+
if (!cancelled) {
|
| 157 |
+
setLoadingHfModels(false);
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}, 250);
|
| 161 |
+
|
| 162 |
+
return () => {
|
| 163 |
+
cancelled = true;
|
| 164 |
+
clearTimeout(timer);
|
| 165 |
+
};
|
| 166 |
+
}, [hfModelQuery]);
|
| 167 |
+
|
| 168 |
async function fetchRequests() {
|
| 169 |
try {
|
| 170 |
const [distillRes, datasetRes] = await Promise.all([
|
|
|
|
| 580 |
<div className="space-y-2">
|
| 581 |
<Label htmlFor="sourceModel">Source Model *</Label>
|
| 582 |
<Combobox
|
| 583 |
+
options={SOURCE_MODELS.map((m) => ({ id: m, name: m }))}
|
| 584 |
value={sourceModel}
|
| 585 |
onValueChange={(v) => {
|
| 586 |
setSourceModel(v);
|
| 587 |
+
if (v !== "Other") {
|
| 588 |
+
setSourceModelOther("");
|
| 589 |
+
setHfModelQuery("");
|
| 590 |
+
setHuggingfaceModels([]);
|
| 591 |
+
}
|
| 592 |
}}
|
| 593 |
+
placeholder="Select a model"
|
| 594 |
searchPlaceholder="Search models..."
|
| 595 |
emptyMessage="No models found"
|
|
|
|
| 596 |
/>
|
| 597 |
</div>
|
| 598 |
<div className="space-y-2">
|
|
|
|
| 613 |
</div>
|
| 614 |
{sourceModel === "Other" && (
|
| 615 |
<div className="space-y-2">
|
| 616 |
+
<Label htmlFor="sourceModelOther">Hugging Face Model *</Label>
|
| 617 |
+
<Combobox
|
| 618 |
+
options={huggingfaceModels}
|
|
|
|
| 619 |
value={sourceModelOther}
|
| 620 |
+
onValueChange={setSourceModelOther}
|
| 621 |
+
placeholder="Search safetensors models"
|
| 622 |
+
searchPlaceholder="Type to search models..."
|
| 623 |
+
emptyMessage={hfModelQuery.trim() ? "No models found" : "Start typing to search"}
|
| 624 |
+
loading={loadingHfModels}
|
| 625 |
+
searchValue={hfModelQuery}
|
| 626 |
+
onSearchValueChange={setHfModelQuery}
|
| 627 |
+
disableLocalFilter
|
| 628 |
/>
|
| 629 |
</div>
|
| 630 |
)}
|
src/components/ui/combobox.tsx
CHANGED
|
@@ -19,6 +19,9 @@ interface ComboboxProps {
|
|
| 19 |
searchPlaceholder?: string;
|
| 20 |
emptyMessage?: string;
|
| 21 |
loading?: boolean;
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
export function Combobox({
|
|
@@ -29,11 +32,17 @@ export function Combobox({
|
|
| 29 |
searchPlaceholder = "Search...",
|
| 30 |
emptyMessage = "No results found.",
|
| 31 |
loading = false,
|
|
|
|
|
|
|
|
|
|
| 32 |
}: ComboboxProps) {
|
| 33 |
const [open, setOpen] = React.useState(false);
|
| 34 |
-
const [
|
|
|
|
|
|
|
| 35 |
|
| 36 |
const filteredOptions = React.useMemo(() => {
|
|
|
|
| 37 |
if (!search) return options;
|
| 38 |
const lower = search.toLowerCase();
|
| 39 |
return options.filter(
|
|
@@ -41,7 +50,7 @@ export function Combobox({
|
|
| 41 |
option.name.toLowerCase().includes(lower) ||
|
| 42 |
option.id.toLowerCase().includes(lower)
|
| 43 |
);
|
| 44 |
-
}, [options, search]);
|
| 45 |
|
| 46 |
const selectedOption = options.find((opt) => opt.id === value);
|
| 47 |
|
|
@@ -65,7 +74,14 @@ export function Combobox({
|
|
| 65 |
<Input
|
| 66 |
placeholder={searchPlaceholder}
|
| 67 |
value={search}
|
| 68 |
-
onChange={(e) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
className="h-9"
|
| 70 |
/>
|
| 71 |
</div>
|
|
@@ -86,7 +102,11 @@ export function Combobox({
|
|
| 86 |
onClick={() => {
|
| 87 |
onValueChange(option.id === value ? "" : option.id);
|
| 88 |
setOpen(false);
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}}
|
| 91 |
className={cn(
|
| 92 |
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
|
|
|
|
| 19 |
searchPlaceholder?: string;
|
| 20 |
emptyMessage?: string;
|
| 21 |
loading?: boolean;
|
| 22 |
+
searchValue?: string;
|
| 23 |
+
onSearchValueChange?: (value: string) => void;
|
| 24 |
+
disableLocalFilter?: boolean;
|
| 25 |
}
|
| 26 |
|
| 27 |
export function Combobox({
|
|
|
|
| 32 |
searchPlaceholder = "Search...",
|
| 33 |
emptyMessage = "No results found.",
|
| 34 |
loading = false,
|
| 35 |
+
searchValue,
|
| 36 |
+
onSearchValueChange,
|
| 37 |
+
disableLocalFilter = false,
|
| 38 |
}: ComboboxProps) {
|
| 39 |
const [open, setOpen] = React.useState(false);
|
| 40 |
+
const [internalSearch, setInternalSearch] = React.useState("");
|
| 41 |
+
|
| 42 |
+
const search = searchValue ?? internalSearch;
|
| 43 |
|
| 44 |
const filteredOptions = React.useMemo(() => {
|
| 45 |
+
if (disableLocalFilter) return options;
|
| 46 |
if (!search) return options;
|
| 47 |
const lower = search.toLowerCase();
|
| 48 |
return options.filter(
|
|
|
|
| 50 |
option.name.toLowerCase().includes(lower) ||
|
| 51 |
option.id.toLowerCase().includes(lower)
|
| 52 |
);
|
| 53 |
+
}, [disableLocalFilter, options, search]);
|
| 54 |
|
| 55 |
const selectedOption = options.find((opt) => opt.id === value);
|
| 56 |
|
|
|
|
| 74 |
<Input
|
| 75 |
placeholder={searchPlaceholder}
|
| 76 |
value={search}
|
| 77 |
+
onChange={(e) => {
|
| 78 |
+
const next = e.target.value;
|
| 79 |
+
if (onSearchValueChange) {
|
| 80 |
+
onSearchValueChange(next);
|
| 81 |
+
} else {
|
| 82 |
+
setInternalSearch(next);
|
| 83 |
+
}
|
| 84 |
+
}}
|
| 85 |
className="h-9"
|
| 86 |
/>
|
| 87 |
</div>
|
|
|
|
| 102 |
onClick={() => {
|
| 103 |
onValueChange(option.id === value ? "" : option.id);
|
| 104 |
setOpen(false);
|
| 105 |
+
if (onSearchValueChange) {
|
| 106 |
+
onSearchValueChange("");
|
| 107 |
+
} else {
|
| 108 |
+
setInternalSearch("");
|
| 109 |
+
}
|
| 110 |
}}
|
| 111 |
className={cn(
|
| 112 |
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
|
src/lib/store.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import fs from "fs";
|
| 3 |
-
import path from "path";
|
| 4 |
|
| 5 |
export interface DistillationRequest {
|
| 6 |
id: string;
|
|
@@ -53,357 +51,428 @@ interface Store {
|
|
| 53 |
threads: Record<string, DiscussionThread>;
|
| 54 |
}
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
function
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
-
function
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
requestType,
|
| 80 |
-
requestId: String(t?.requestId ?? ""),
|
| 81 |
-
comments: Array.isArray(t?.comments)
|
| 82 |
-
? t.comments.map((c: any) => ({
|
| 83 |
-
id: String(c?.id ?? uuidv4()),
|
| 84 |
-
body: String(c?.body ?? ""),
|
| 85 |
-
author: String(c?.author ?? (c?.role === "user" ? "Anonymous" : "TeichAI")),
|
| 86 |
-
role: c?.role === "user" ? "user" : "admin",
|
| 87 |
-
ownerId: String(c?.ownerId ?? ""),
|
| 88 |
-
createdAt: String(c?.createdAt ?? new Date().toISOString()),
|
| 89 |
-
editedAt: c?.editedAt ? String(c.editedAt) : undefined,
|
| 90 |
-
}))
|
| 91 |
-
: [],
|
| 92 |
-
};
|
| 93 |
-
return [key, thread] as const;
|
| 94 |
-
})
|
| 95 |
-
)
|
| 96 |
-
: {};
|
| 97 |
-
const store: Store = {
|
| 98 |
-
distillationRequests: Array.isArray(parsed.distillationRequests)
|
| 99 |
-
? (parsed.distillationRequests as any[]).map((r) => ({
|
| 100 |
-
id: String(r.id ?? uuidv4()),
|
| 101 |
-
sourceDataset: String(r.sourceDataset ?? r.teacherModel ?? ""),
|
| 102 |
-
studentModel: String(r.studentModel ?? ""),
|
| 103 |
-
additionalNotes: String(r.additionalNotes ?? ""),
|
| 104 |
-
upvotes: typeof r.upvotes === "number" ? r.upvotes : 0,
|
| 105 |
-
votedIps: Array.isArray(r.votedIps) ? r.votedIps.map(String) : [],
|
| 106 |
-
ownerId: String(r.ownerId ?? ""),
|
| 107 |
-
createdAt: String(r.createdAt ?? new Date().toISOString()),
|
| 108 |
-
status: (r.status === "in_progress" || r.status === "completed") ? r.status : "pending",
|
| 109 |
-
}))
|
| 110 |
-
: [],
|
| 111 |
-
datasetRequests: Array.isArray(parsed.datasetRequests)
|
| 112 |
-
? (parsed.datasetRequests as any[]).map((r) => ({
|
| 113 |
-
id: String(r.id ?? uuidv4()),
|
| 114 |
-
sourceModel: String(r.sourceModel ?? ""),
|
| 115 |
-
datasetSize: String(r.datasetSize ?? "250x"),
|
| 116 |
-
reasoningDepth: String(r.reasoningDepth ?? "high"),
|
| 117 |
-
topics: Array.isArray(r.topics) ? r.topics.map(String) : [],
|
| 118 |
-
additionalNotes: String(r.additionalNotes ?? ""),
|
| 119 |
-
upvotes: typeof r.upvotes === "number" ? r.upvotes : 0,
|
| 120 |
-
votedIps: Array.isArray(r.votedIps) ? r.votedIps.map(String) : [],
|
| 121 |
-
ownerId: String(r.ownerId ?? ""),
|
| 122 |
-
createdAt: String(r.createdAt ?? new Date().toISOString()),
|
| 123 |
-
status: (r.status === "in_progress" || r.status === "completed") ? r.status : "pending",
|
| 124 |
-
}))
|
| 125 |
-
: [],
|
| 126 |
-
threads,
|
| 127 |
-
};
|
| 128 |
-
return store;
|
| 129 |
-
}
|
| 130 |
-
} catch (error) {
|
| 131 |
-
console.error("Error loading store:", error);
|
| 132 |
-
}
|
| 133 |
-
return { distillationRequests: [], datasetRequests: [], threads: {} };
|
| 134 |
}
|
| 135 |
|
| 136 |
-
function
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
-
export function getDistillationRequests(): DistillationRequest[] {
|
| 142 |
-
const
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
| 145 |
|
| 146 |
-
export function getDatasetRequests(): DatasetRequest[] {
|
| 147 |
-
const
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
-
export function getRequest(type: RequestType, id: string): DistillationRequest | DatasetRequest | null {
|
| 152 |
-
const
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
}
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
-
export function updateDistillationRequest(
|
| 160 |
id: string,
|
| 161 |
updates: Partial<Pick<DistillationRequest, "sourceDataset" | "studentModel" | "additionalNotes">>
|
| 162 |
-
): DistillationRequest | null {
|
| 163 |
-
const
|
| 164 |
-
|
| 165 |
-
if (
|
| 166 |
-
if (typeof updates.
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
-
export function updateDatasetRequest(
|
| 174 |
id: string,
|
| 175 |
updates: Partial<Pick<DatasetRequest, "sourceModel" | "datasetSize" | "reasoningDepth" | "topics" | "additionalNotes">>
|
| 176 |
-
): DatasetRequest | null {
|
| 177 |
-
const
|
| 178 |
-
|
| 179 |
-
if (
|
| 180 |
-
if (typeof updates.
|
| 181 |
-
if (
|
| 182 |
-
if (typeof updates.
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
|
| 189 |
-
export function updateRequestStatus(
|
| 190 |
type: RequestType,
|
| 191 |
id: string,
|
| 192 |
status: "pending" | "in_progress" | "completed"
|
| 193 |
-
): boolean {
|
| 194 |
-
const
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
|
| 203 |
-
|
| 204 |
-
if (!request) return false;
|
| 205 |
-
request.status = status;
|
| 206 |
-
saveStore(store);
|
| 207 |
-
return true;
|
| 208 |
}
|
| 209 |
|
| 210 |
-
export function deleteRequest(type: RequestType, id: string): boolean {
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
store.datasetRequests = store.datasetRequests.filter((r) => r.id !== id);
|
| 219 |
-
if (store.datasetRequests.length === before) return false;
|
| 220 |
-
}
|
| 221 |
|
| 222 |
-
const
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
}
|
| 226 |
-
|
| 227 |
-
return
|
| 228 |
}
|
| 229 |
|
| 230 |
-
export function getThread(type: RequestType, id: string): DiscussionThread {
|
| 231 |
-
const store = loadStore();
|
| 232 |
const key = `${type}:${id}`;
|
| 233 |
-
const
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
-
|
|
|
|
| 238 |
key,
|
| 239 |
requestType: type,
|
| 240 |
requestId: id,
|
| 241 |
-
comments: [],
|
| 242 |
};
|
| 243 |
-
store.threads[key] = thread;
|
| 244 |
-
saveStore(store);
|
| 245 |
-
return thread;
|
| 246 |
}
|
| 247 |
|
| 248 |
-
export function addAdminComment(type: RequestType, id: string, body: string, ownerId: string): DiscussionComment {
|
| 249 |
-
const
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
}
|
| 270 |
|
| 271 |
-
export function addUserComment(
|
| 272 |
type: RequestType,
|
| 273 |
id: string,
|
| 274 |
body: string,
|
| 275 |
author: string | undefined,
|
| 276 |
ownerId: string
|
| 277 |
-
): DiscussionComment {
|
| 278 |
-
const
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
}
|
| 299 |
|
| 300 |
-
export function updateComment(
|
| 301 |
type: RequestType,
|
| 302 |
requestId: string,
|
| 303 |
commentId: string,
|
| 304 |
body: string
|
| 305 |
-
): DiscussionComment | null {
|
| 306 |
-
const
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
|
| 318 |
-
export function deleteComment(
|
| 319 |
type: RequestType,
|
| 320 |
requestId: string,
|
| 321 |
commentId: string
|
| 322 |
-
): boolean {
|
| 323 |
-
const
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
}
|
| 334 |
|
| 335 |
-
export function addDistillationRequest(
|
| 336 |
request: Omit<DistillationRequest, "id" | "upvotes" | "votedIps" | "createdAt" | "status">
|
| 337 |
-
): DistillationRequest {
|
| 338 |
-
const
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
}
|
| 351 |
|
| 352 |
-
export function addDatasetRequest(
|
| 353 |
request: Omit<DatasetRequest, "id" | "upvotes" | "votedIps" | "createdAt" | "status">
|
| 354 |
-
): DatasetRequest {
|
| 355 |
-
const
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
}
|
| 368 |
|
| 369 |
-
export function upvoteDistillation(
|
| 370 |
id: string,
|
| 371 |
ip: string
|
| 372 |
-
): { success: boolean; upvotes: number; action?: "upvoted" | "unvoted" } {
|
| 373 |
-
const
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
return { success: true, upvotes: request.upvotes, action: "unvoted" };
|
| 383 |
}
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
-
export function upvoteDataset(
|
| 391 |
id: string,
|
| 392 |
ip: string
|
| 393 |
-
): { success: boolean; upvotes: number; action?: "upvoted" | "unvoted" } {
|
| 394 |
-
const
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
| 398 |
}
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
return { success: true, upvotes: request.upvotes, action: "unvoted" };
|
| 404 |
}
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
| 409 |
}
|
|
|
|
| 1 |
+
import { supabaseRest } from "@/lib/supabaseRest";
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export interface DistillationRequest {
|
| 4 |
id: string;
|
|
|
|
| 51 |
threads: Record<string, DiscussionThread>;
|
| 52 |
}
|
| 53 |
|
| 54 |
+
function normalizeStatus(value: unknown): "pending" | "in_progress" | "completed" {
|
| 55 |
+
return value === "in_progress" || value === "completed" ? value : "pending";
|
| 56 |
+
}
|
| 57 |
|
| 58 |
+
function mapDistillationRow(row: any): DistillationRequest {
|
| 59 |
+
return {
|
| 60 |
+
id: String(row?.id ?? ""),
|
| 61 |
+
sourceDataset: String(row?.source_dataset ?? ""),
|
| 62 |
+
studentModel: String(row?.student_model ?? ""),
|
| 63 |
+
additionalNotes: String(row?.additional_notes ?? ""),
|
| 64 |
+
upvotes: typeof row?.upvotes === "number" ? row.upvotes : 0,
|
| 65 |
+
votedIps: Array.isArray(row?.voted_ips) ? row.voted_ips.map(String) : [],
|
| 66 |
+
ownerId: String(row?.owner_id ?? ""),
|
| 67 |
+
createdAt: String(row?.created_at ?? new Date().toISOString()),
|
| 68 |
+
status: normalizeStatus(row?.status),
|
| 69 |
+
};
|
| 70 |
}
|
| 71 |
|
| 72 |
+
function mapDatasetRow(row: any): DatasetRequest {
|
| 73 |
+
return {
|
| 74 |
+
id: String(row?.id ?? ""),
|
| 75 |
+
sourceModel: String(row?.source_model ?? ""),
|
| 76 |
+
datasetSize: String(row?.dataset_size ?? "250x"),
|
| 77 |
+
reasoningDepth: String(row?.reasoning_depth ?? "high"),
|
| 78 |
+
topics: Array.isArray(row?.topics) ? row.topics.map(String) : [],
|
| 79 |
+
additionalNotes: String(row?.additional_notes ?? ""),
|
| 80 |
+
upvotes: typeof row?.upvotes === "number" ? row.upvotes : 0,
|
| 81 |
+
votedIps: Array.isArray(row?.voted_ips) ? row.voted_ips.map(String) : [],
|
| 82 |
+
ownerId: String(row?.owner_id ?? ""),
|
| 83 |
+
createdAt: String(row?.created_at ?? new Date().toISOString()),
|
| 84 |
+
status: normalizeStatus(row?.status),
|
| 85 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
+
function mapCommentRow(row: any): DiscussionComment {
|
| 89 |
+
return {
|
| 90 |
+
id: String(row?.id ?? ""),
|
| 91 |
+
body: String(row?.body ?? ""),
|
| 92 |
+
author: String(row?.author ?? (row?.role === "user" ? "Anonymous" : "TeichAI")),
|
| 93 |
+
role: row?.role === "user" ? "user" : "admin",
|
| 94 |
+
ownerId: String(row?.owner_id ?? ""),
|
| 95 |
+
createdAt: String(row?.created_at ?? new Date().toISOString()),
|
| 96 |
+
editedAt: row?.edited_at ? String(row.edited_at) : undefined,
|
| 97 |
+
};
|
| 98 |
}
|
| 99 |
|
| 100 |
+
export async function getDistillationRequests(): Promise<DistillationRequest[]> {
|
| 101 |
+
const { data, error } = await supabaseRest<any[]>("/rest/v1/distillation_requests", {
|
| 102 |
+
query: {
|
| 103 |
+
select: "*",
|
| 104 |
+
order: "upvotes.desc,created_at.desc",
|
| 105 |
+
},
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
if (error) {
|
| 109 |
+
throw new Error(error.message);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return Array.isArray(data) ? data.map(mapDistillationRow) : [];
|
| 113 |
}
|
| 114 |
|
| 115 |
+
export async function getDatasetRequests(): Promise<DatasetRequest[]> {
|
| 116 |
+
const { data, error } = await supabaseRest<any[]>("/rest/v1/dataset_requests", {
|
| 117 |
+
query: {
|
| 118 |
+
select: "*",
|
| 119 |
+
order: "upvotes.desc,created_at.desc",
|
| 120 |
+
},
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
if (error) {
|
| 124 |
+
throw new Error(error.message);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return Array.isArray(data) ? data.map(mapDatasetRow) : [];
|
| 128 |
}
|
| 129 |
|
| 130 |
+
export async function getRequest(type: RequestType, id: string): Promise<DistillationRequest | DatasetRequest | null> {
|
| 131 |
+
const table = type === "distillation" ? "distillation_requests" : "dataset_requests";
|
| 132 |
+
const { data, error } = await supabaseRest<any>(`/rest/v1/${table}`, {
|
| 133 |
+
query: {
|
| 134 |
+
select: "*",
|
| 135 |
+
id: `eq.${id}`,
|
| 136 |
+
},
|
| 137 |
+
acceptObject: true,
|
| 138 |
+
});
|
| 139 |
+
|
| 140 |
+
if (error) {
|
| 141 |
+
throw new Error(error.message);
|
| 142 |
}
|
| 143 |
+
|
| 144 |
+
if (!data) return null;
|
| 145 |
+
return type === "distillation" ? mapDistillationRow(data) : mapDatasetRow(data);
|
| 146 |
}
|
| 147 |
|
| 148 |
+
export async function updateDistillationRequest(
|
| 149 |
id: string,
|
| 150 |
updates: Partial<Pick<DistillationRequest, "sourceDataset" | "studentModel" | "additionalNotes">>
|
| 151 |
+
): Promise<DistillationRequest | null> {
|
| 152 |
+
const body: Record<string, any> = {};
|
| 153 |
+
if (typeof updates.sourceDataset === "string") body.source_dataset = updates.sourceDataset;
|
| 154 |
+
if (typeof updates.studentModel === "string") body.student_model = updates.studentModel;
|
| 155 |
+
if (typeof updates.additionalNotes === "string") body.additional_notes = updates.additionalNotes;
|
| 156 |
+
|
| 157 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/distillation_requests", {
|
| 158 |
+
method: "PATCH",
|
| 159 |
+
body: JSON.stringify(body),
|
| 160 |
+
query: { select: "*", id: `eq.${id}` },
|
| 161 |
+
preferReturn: "representation",
|
| 162 |
+
acceptObject: true,
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
if (error) {
|
| 166 |
+
throw new Error(error.message);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
return data ? mapDistillationRow(data) : null;
|
| 170 |
}
|
| 171 |
|
| 172 |
+
export async function updateDatasetRequest(
|
| 173 |
id: string,
|
| 174 |
updates: Partial<Pick<DatasetRequest, "sourceModel" | "datasetSize" | "reasoningDepth" | "topics" | "additionalNotes">>
|
| 175 |
+
): Promise<DatasetRequest | null> {
|
| 176 |
+
const body: Record<string, any> = {};
|
| 177 |
+
if (typeof updates.sourceModel === "string") body.source_model = updates.sourceModel;
|
| 178 |
+
if (typeof updates.datasetSize === "string") body.dataset_size = updates.datasetSize;
|
| 179 |
+
if (typeof updates.reasoningDepth === "string") body.reasoning_depth = updates.reasoningDepth;
|
| 180 |
+
if (Array.isArray(updates.topics)) body.topics = updates.topics.map(String);
|
| 181 |
+
if (typeof updates.additionalNotes === "string") body.additional_notes = updates.additionalNotes;
|
| 182 |
+
|
| 183 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/dataset_requests", {
|
| 184 |
+
method: "PATCH",
|
| 185 |
+
body: JSON.stringify(body),
|
| 186 |
+
query: { select: "*", id: `eq.${id}` },
|
| 187 |
+
preferReturn: "representation",
|
| 188 |
+
acceptObject: true,
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
if (error) {
|
| 192 |
+
throw new Error(error.message);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
return data ? mapDatasetRow(data) : null;
|
| 196 |
}
|
| 197 |
|
| 198 |
+
export async function updateRequestStatus(
|
| 199 |
type: RequestType,
|
| 200 |
id: string,
|
| 201 |
status: "pending" | "in_progress" | "completed"
|
| 202 |
+
): Promise<boolean> {
|
| 203 |
+
const table = type === "distillation" ? "distillation_requests" : "dataset_requests";
|
| 204 |
+
const { data, error } = await supabaseRest<any>(`/rest/v1/${table}`, {
|
| 205 |
+
method: "PATCH",
|
| 206 |
+
body: JSON.stringify({ status }),
|
| 207 |
+
query: { select: "id", id: `eq.${id}` },
|
| 208 |
+
preferReturn: "representation",
|
| 209 |
+
acceptObject: true,
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
if (error) {
|
| 213 |
+
throw new Error(error.message);
|
| 214 |
}
|
| 215 |
|
| 216 |
+
return Boolean(data?.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
+
export async function deleteRequest(type: RequestType, id: string): Promise<boolean> {
|
| 220 |
+
await supabaseRest("/rest/v1/request_comments", {
|
| 221 |
+
method: "DELETE",
|
| 222 |
+
query: {
|
| 223 |
+
request_type: `eq.${type}`,
|
| 224 |
+
request_id: `eq.${id}`,
|
| 225 |
+
},
|
| 226 |
+
});
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
+
const table = type === "distillation" ? "distillation_requests" : "dataset_requests";
|
| 229 |
+
const { data, error } = await supabaseRest<any>(`/rest/v1/${table}`, {
|
| 230 |
+
method: "DELETE",
|
| 231 |
+
body: null,
|
| 232 |
+
query: { select: "id", id: `eq.${id}` },
|
| 233 |
+
preferReturn: "representation",
|
| 234 |
+
acceptObject: true,
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
if (error) {
|
| 238 |
+
throw new Error(error.message);
|
| 239 |
}
|
| 240 |
+
|
| 241 |
+
return Boolean(data?.id);
|
| 242 |
}
|
| 243 |
|
| 244 |
+
export async function getThread(type: RequestType, id: string): Promise<DiscussionThread> {
|
|
|
|
| 245 |
const key = `${type}:${id}`;
|
| 246 |
+
const { data, error } = await supabaseRest<any[]>("/rest/v1/request_comments", {
|
| 247 |
+
query: {
|
| 248 |
+
select: "*",
|
| 249 |
+
request_type: `eq.${type}`,
|
| 250 |
+
request_id: `eq.${id}`,
|
| 251 |
+
order: "created_at.asc",
|
| 252 |
+
},
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
if (error) {
|
| 256 |
+
throw new Error(error.message);
|
| 257 |
}
|
| 258 |
+
|
| 259 |
+
return {
|
| 260 |
key,
|
| 261 |
requestType: type,
|
| 262 |
requestId: id,
|
| 263 |
+
comments: Array.isArray(data) ? data.map(mapCommentRow) : [],
|
| 264 |
};
|
|
|
|
|
|
|
|
|
|
| 265 |
}
|
| 266 |
|
| 267 |
+
export async function addAdminComment(type: RequestType, id: string, body: string, ownerId: string): Promise<DiscussionComment> {
|
| 268 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/request_comments", {
|
| 269 |
+
method: "POST",
|
| 270 |
+
body: JSON.stringify({
|
| 271 |
+
request_type: type,
|
| 272 |
+
request_id: id,
|
| 273 |
+
body,
|
| 274 |
+
author: "TeichAI",
|
| 275 |
+
role: "admin",
|
| 276 |
+
owner_id: ownerId,
|
| 277 |
+
}),
|
| 278 |
+
query: { select: "*" },
|
| 279 |
+
preferReturn: "representation",
|
| 280 |
+
acceptObject: true,
|
| 281 |
+
});
|
| 282 |
+
|
| 283 |
+
if (error) {
|
| 284 |
+
throw new Error(error.message);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
if (!data) {
|
| 288 |
+
throw new Error("Failed to create comment");
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
return mapCommentRow(data);
|
| 292 |
}
|
| 293 |
|
| 294 |
+
export async function addUserComment(
|
| 295 |
type: RequestType,
|
| 296 |
id: string,
|
| 297 |
body: string,
|
| 298 |
author: string | undefined,
|
| 299 |
ownerId: string
|
| 300 |
+
): Promise<DiscussionComment> {
|
| 301 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/request_comments", {
|
| 302 |
+
method: "POST",
|
| 303 |
+
body: JSON.stringify({
|
| 304 |
+
request_type: type,
|
| 305 |
+
request_id: id,
|
| 306 |
+
body,
|
| 307 |
+
author: author?.trim() ? author.trim() : "Anonymous",
|
| 308 |
+
role: "user",
|
| 309 |
+
owner_id: ownerId,
|
| 310 |
+
}),
|
| 311 |
+
query: { select: "*" },
|
| 312 |
+
preferReturn: "representation",
|
| 313 |
+
acceptObject: true,
|
| 314 |
+
});
|
| 315 |
+
|
| 316 |
+
if (error) {
|
| 317 |
+
throw new Error(error.message);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
if (!data) {
|
| 321 |
+
throw new Error("Failed to create comment");
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
return mapCommentRow(data);
|
| 325 |
}
|
| 326 |
|
| 327 |
+
export async function updateComment(
|
| 328 |
type: RequestType,
|
| 329 |
requestId: string,
|
| 330 |
commentId: string,
|
| 331 |
body: string
|
| 332 |
+
): Promise<DiscussionComment | null> {
|
| 333 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/request_comments", {
|
| 334 |
+
method: "PATCH",
|
| 335 |
+
body: JSON.stringify({ body, edited_at: new Date().toISOString() }),
|
| 336 |
+
query: {
|
| 337 |
+
select: "*",
|
| 338 |
+
id: `eq.${commentId}`,
|
| 339 |
+
request_type: `eq.${type}`,
|
| 340 |
+
request_id: `eq.${requestId}`,
|
| 341 |
+
},
|
| 342 |
+
preferReturn: "representation",
|
| 343 |
+
acceptObject: true,
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
if (error) {
|
| 347 |
+
throw new Error(error.message);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
return data ? mapCommentRow(data) : null;
|
| 351 |
}
|
| 352 |
|
| 353 |
+
export async function deleteComment(
|
| 354 |
type: RequestType,
|
| 355 |
requestId: string,
|
| 356 |
commentId: string
|
| 357 |
+
): Promise<boolean> {
|
| 358 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/request_comments", {
|
| 359 |
+
method: "DELETE",
|
| 360 |
+
body: null,
|
| 361 |
+
query: {
|
| 362 |
+
select: "id",
|
| 363 |
+
id: `eq.${commentId}`,
|
| 364 |
+
request_type: `eq.${type}`,
|
| 365 |
+
request_id: `eq.${requestId}`,
|
| 366 |
+
},
|
| 367 |
+
preferReturn: "representation",
|
| 368 |
+
acceptObject: true,
|
| 369 |
+
});
|
| 370 |
+
|
| 371 |
+
if (error) {
|
| 372 |
+
throw new Error(error.message);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
return Boolean(data?.id);
|
| 376 |
}
|
| 377 |
|
| 378 |
+
export async function addDistillationRequest(
|
| 379 |
request: Omit<DistillationRequest, "id" | "upvotes" | "votedIps" | "createdAt" | "status">
|
| 380 |
+
): Promise<DistillationRequest> {
|
| 381 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/distillation_requests", {
|
| 382 |
+
method: "POST",
|
| 383 |
+
body: JSON.stringify({
|
| 384 |
+
source_dataset: request.sourceDataset,
|
| 385 |
+
student_model: request.studentModel,
|
| 386 |
+
additional_notes: request.additionalNotes,
|
| 387 |
+
owner_id: request.ownerId,
|
| 388 |
+
}),
|
| 389 |
+
query: { select: "*" },
|
| 390 |
+
preferReturn: "representation",
|
| 391 |
+
acceptObject: true,
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
if (error) {
|
| 395 |
+
throw new Error(error.message);
|
| 396 |
+
}
|
| 397 |
+
if (!data) {
|
| 398 |
+
throw new Error("Failed to create request");
|
| 399 |
+
}
|
| 400 |
+
return mapDistillationRow(data);
|
| 401 |
}
|
| 402 |
|
| 403 |
+
export async function addDatasetRequest(
|
| 404 |
request: Omit<DatasetRequest, "id" | "upvotes" | "votedIps" | "createdAt" | "status">
|
| 405 |
+
): Promise<DatasetRequest> {
|
| 406 |
+
const { data, error } = await supabaseRest<any>("/rest/v1/dataset_requests", {
|
| 407 |
+
method: "POST",
|
| 408 |
+
body: JSON.stringify({
|
| 409 |
+
source_model: request.sourceModel,
|
| 410 |
+
dataset_size: request.datasetSize,
|
| 411 |
+
reasoning_depth: request.reasoningDepth,
|
| 412 |
+
topics: request.topics,
|
| 413 |
+
additional_notes: request.additionalNotes,
|
| 414 |
+
owner_id: request.ownerId,
|
| 415 |
+
}),
|
| 416 |
+
query: { select: "*" },
|
| 417 |
+
preferReturn: "representation",
|
| 418 |
+
acceptObject: true,
|
| 419 |
+
});
|
| 420 |
+
|
| 421 |
+
if (error) {
|
| 422 |
+
throw new Error(error.message);
|
| 423 |
+
}
|
| 424 |
+
if (!data) {
|
| 425 |
+
throw new Error("Failed to create request");
|
| 426 |
+
}
|
| 427 |
+
return mapDatasetRow(data);
|
| 428 |
}
|
| 429 |
|
| 430 |
+
export async function upvoteDistillation(
|
| 431 |
id: string,
|
| 432 |
ip: string
|
| 433 |
+
): Promise<{ success: boolean; upvotes: number; action?: "upvoted" | "unvoted" }> {
|
| 434 |
+
const { data, error } = await supabaseRest<any[]>("/rest/v1/rpc/toggle_upvote_distillation", {
|
| 435 |
+
method: "POST",
|
| 436 |
+
body: JSON.stringify({ request_id: id, voter_ip: ip }),
|
| 437 |
+
});
|
| 438 |
+
|
| 439 |
+
if (error) {
|
| 440 |
+
throw new Error(error.message);
|
| 441 |
}
|
| 442 |
+
|
| 443 |
+
const row = Array.isArray(data) ? data[0] : null;
|
| 444 |
+
if (!row) {
|
| 445 |
+
return { success: false, upvotes: 0 };
|
|
|
|
| 446 |
}
|
| 447 |
+
|
| 448 |
+
return {
|
| 449 |
+
success: Boolean(row.success),
|
| 450 |
+
upvotes: typeof row.upvotes === "number" ? row.upvotes : 0,
|
| 451 |
+
action: row.action === "upvoted" || row.action === "unvoted" ? row.action : undefined,
|
| 452 |
+
};
|
| 453 |
}
|
| 454 |
|
| 455 |
+
export async function upvoteDataset(
|
| 456 |
id: string,
|
| 457 |
ip: string
|
| 458 |
+
): Promise<{ success: boolean; upvotes: number; action?: "upvoted" | "unvoted" }> {
|
| 459 |
+
const { data, error } = await supabaseRest<any[]>("/rest/v1/rpc/toggle_upvote_dataset", {
|
| 460 |
+
method: "POST",
|
| 461 |
+
body: JSON.stringify({ request_id: id, voter_ip: ip }),
|
| 462 |
+
});
|
| 463 |
+
|
| 464 |
+
if (error) {
|
| 465 |
+
throw new Error(error.message);
|
| 466 |
}
|
| 467 |
+
|
| 468 |
+
const row = Array.isArray(data) ? data[0] : null;
|
| 469 |
+
if (!row) {
|
| 470 |
+
return { success: false, upvotes: 0 };
|
|
|
|
| 471 |
}
|
| 472 |
+
|
| 473 |
+
return {
|
| 474 |
+
success: Boolean(row.success),
|
| 475 |
+
upvotes: typeof row.upvotes === "number" ? row.upvotes : 0,
|
| 476 |
+
action: row.action === "upvoted" || row.action === "unvoted" ? row.action : undefined,
|
| 477 |
+
};
|
| 478 |
}
|