feat(infra): add tools service with document scanner, image & PDF tools
Implement self-hosted document scanner and media processing tools as an alternative to CamScanner/ilovepdf without third-party uploads. Backend: Rust Axum gateway + worker pool with NATS JetStream queue Frontend: Next.js 16 + shadcn/ui + Tailwind v4 + Framer Motion Pipeline: Canny edge detection -> DLT homography warp -> Sauvola binarization -> Hough deskew -> Tesseract OCR -> searchable PDF Phase 1 (MVP) delivers: - Document scanner with perspective correction and OCR - Image compress/resize/convert tools - PDF merge/split/compress tools - Real-time WebSocket progress updates - Rate limiting, auto-cleanup, Prometheus metrics - Full CI/CD pipeline with Docker multi-stage build Co-Authored-By: Kilo <kilo@kilo.ai>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { VideoIcon } from "lucide-react";
|
||||
import { ToolLayout } from "@/components/tools/tool-layout";
|
||||
import { UploadZone } from "@/components/tools/upload-zone";
|
||||
import { useUpload } from "@/hooks/use-upload";
|
||||
import { useJobStatus } from "@/hooks/use-job-status";
|
||||
import { ProgressBar } from "@/components/tools/progress-bar";
|
||||
import { ResultPreview } from "@/components/tools/result-preview";
|
||||
|
||||
type PageState = "upload" | "processing" | "result" | "error";
|
||||
|
||||
export default function VideoCompressPage() {
|
||||
const [pageState, setPageState] = useState<PageState>("upload");
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
const { upload } = useUpload({
|
||||
tool: "video-compress",
|
||||
options: { quality: 80 },
|
||||
});
|
||||
|
||||
const handleComplete = useCallback(() => setPageState("result"), []);
|
||||
const handleError = useCallback(
|
||||
(err: string) => {
|
||||
setErrorMsg(err);
|
||||
setPageState("error");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const { progress, stage, message, status, result } = useJobStatus(jobId, {
|
||||
onComplete: handleComplete,
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const handleUpload = useCallback(
|
||||
async (file: File) => {
|
||||
setErrorMsg(null);
|
||||
const res = await upload(file);
|
||||
if (res) {
|
||||
setJobId(res.job_id);
|
||||
setPageState("processing");
|
||||
}
|
||||
},
|
||||
[upload],
|
||||
);
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
setPageState("upload");
|
||||
setJobId(null);
|
||||
setErrorMsg(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ToolLayout
|
||||
title="Compress Video"
|
||||
description="Turunin bitrate & resolusi"
|
||||
icon={VideoIcon}
|
||||
phase={3}
|
||||
>
|
||||
{pageState === "upload" && (
|
||||
<UploadZone
|
||||
accept="video/*"
|
||||
tool="video-compress"
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pageState === "processing" && jobId && (
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
stage={stage}
|
||||
message={message}
|
||||
status={status}
|
||||
onRetry={handleRetry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pageState === "result" && result && (
|
||||
<ResultPreview
|
||||
result={result}
|
||||
onProcessAnother={handleRetry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pageState === "error" && (
|
||||
<div className="p-6 rounded-xl border border-destructive/20 bg-destructive/5 text-center">
|
||||
<p className="text-destructive font-medium mb-4">
|
||||
{errorMsg || "Terjadi kesalahan"}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90"
|
||||
>
|
||||
Coba Lagi
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</ToolLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user