fix: audit perbaikan NATS subject, worker spawn, frontend options, pipeline PDF output
Bug fixes: 1. NATS subject doubled prefix — gateway publish ke tools.tools.scan.* padahal workers subscribe ke tools.scan.*. Fix: pakai group() method 2. Worker subscriptions pake tokio::select! — cuma satu subscription yang diproses, sisanya di-cancel. Fix: ganti ke tokio::spawn + loop 3. WebSocket URL hardcoded ke localhost:3001 — gak jalan di prod. Fix: infer dari window.location atau env var 4. Frontend scan options hardcoded — user gak bisa atur OCR/enhance/format. Fix: interactive toggles, selects, slider 5. Frontend compress quality hardcoded — user gak bisa atur kualitas. Fix: quality range slider 6. Pipeline cuma output PNG — gak ada PDF/OCR. Fix: generate searchable PDF kalo tesseract feature enabled Enhancements: - Tool::group() method added — returns 'scan', 'image', 'pdf' dll - Dockerfile: --features tesseract pas cargo build - leptess OCR: proper API usage (set_image_from_mem, recognize, etc.) Co-Authored-By: Kilo <kilo@kilo.ai>
This commit is contained in:
@@ -15,10 +15,11 @@ export default function ImageCompressPage() {
|
||||
const [pageState, setPageState] = useState<PageState>("upload");
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [quality, setQuality] = useState(80);
|
||||
|
||||
const { upload } = useUpload({
|
||||
tool: "image-compress",
|
||||
options: { quality: 80 },
|
||||
options: { quality },
|
||||
});
|
||||
|
||||
const handleComplete = useCallback(() => setPageState("result"), []);
|
||||
@@ -61,11 +62,35 @@ export default function ImageCompressPage() {
|
||||
phase={1}
|
||||
>
|
||||
{pageState === "upload" && (
|
||||
<UploadZone
|
||||
accept="image/*"
|
||||
tool="image-compress"
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
{/* Quality Slider */}
|
||||
<div className="p-6 rounded-xl border glass space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Quality</span>
|
||||
<span className="text-sm font-mono text-muted-foreground">
|
||||
{quality}%
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={100}
|
||||
value={quality}
|
||||
onChange={(e) => setQuality(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Kecil</span>
|
||||
<span>Besar</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UploadZone
|
||||
accept="image/*"
|
||||
tool="image-compress"
|
||||
onUpload={handleUpload}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pageState === "processing" && jobId && (
|
||||
@@ -79,10 +104,7 @@ export default function ImageCompressPage() {
|
||||
)}
|
||||
|
||||
{pageState === "result" && result && (
|
||||
<ResultPreview
|
||||
result={result}
|
||||
onProcessAnother={handleRetry}
|
||||
/>
|
||||
<ResultPreview result={result} onProcessAnother={handleRetry} />
|
||||
)}
|
||||
|
||||
{pageState === "error" && (
|
||||
|
||||
@@ -10,14 +10,29 @@ import { useJobStatus } from "@/hooks/use-job-status";
|
||||
|
||||
type PageState = "upload" | "processing" | "result" | "error";
|
||||
|
||||
interface ScanOptions {
|
||||
ocr: boolean;
|
||||
enhance: boolean;
|
||||
output_format: "pdf" | "jpeg" | "png";
|
||||
dpi: number;
|
||||
color_mode: "black_and_white" | "grayscale" | "color";
|
||||
}
|
||||
|
||||
export default function ScanPage() {
|
||||
const [pageState, setPageState] = useState<PageState>("upload");
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [opts, setOpts] = useState<ScanOptions>({
|
||||
ocr: true,
|
||||
enhance: true,
|
||||
output_format: "pdf",
|
||||
dpi: 300,
|
||||
color_mode: "black_and_white",
|
||||
});
|
||||
|
||||
const { upload, isUploading } = useUpload({
|
||||
tool: "scan",
|
||||
options: { ocr: true, enhance: true, output_format: "pdf", dpi: 300 },
|
||||
options: opts as unknown as Record<string, unknown>,
|
||||
});
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
@@ -75,15 +90,83 @@ export default function ScanPage() {
|
||||
maxSizeMB={50}
|
||||
/>
|
||||
|
||||
{/* Options info */}
|
||||
<div className="p-4 rounded-lg border glass text-sm text-muted-foreground">
|
||||
<p className="font-medium text-foreground mb-2">Scan Options</p>
|
||||
<ul className="space-y-1">
|
||||
<li>• OCR: Enabled (English + Indonesian)</li>
|
||||
<li>• Output: Searchable PDF</li>
|
||||
<li>• DPI: 300</li>
|
||||
<li>• Auto-enhance: On</li>
|
||||
</ul>
|
||||
{/* Interactive Options */}
|
||||
<div className="p-6 rounded-xl border glass space-y-4">
|
||||
<h3 className="font-semibold">Scan Options</h3>
|
||||
|
||||
{/* OCR Toggle */}
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">OCR (Tesseract)</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={opts.ocr}
|
||||
onChange={(e) => setOpts({ ...opts, ocr: e.target.checked })}
|
||||
className="toggle"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Enhance Toggle */}
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">Auto-enhance</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={opts.enhance}
|
||||
onChange={(e) => setOpts({ ...opts, enhance: e.target.checked })}
|
||||
className="toggle"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Output Format */}
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">Output Format</span>
|
||||
<select
|
||||
value={opts.output_format}
|
||||
onChange={(e) =>
|
||||
setOpts({
|
||||
...opts,
|
||||
output_format: e.target.value as ScanOptions["output_format"],
|
||||
})
|
||||
}
|
||||
className="bg-muted border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="pdf">Searchable PDF</option>
|
||||
<option value="jpeg">JPEG Image</option>
|
||||
<option value="png">PNG Image</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* Color Mode */}
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">Color Mode</span>
|
||||
<select
|
||||
value={opts.color_mode}
|
||||
onChange={(e) =>
|
||||
setOpts({
|
||||
...opts,
|
||||
color_mode: e.target.value as ScanOptions["color_mode"],
|
||||
})
|
||||
}
|
||||
className="bg-muted border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="black_and_white">Black & White</option>
|
||||
<option value="grayscale">Grayscale</option>
|
||||
<option value="color">Color</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* DPI */}
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">DPI</span>
|
||||
<select
|
||||
value={opts.dpi}
|
||||
onChange={(e) => setOpts({ ...opts, dpi: Number(e.target.value) })}
|
||||
className="bg-muted border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value={150}>150 (draft)</option>
|
||||
<option value={300}>300 (standard)</option>
|
||||
<option value={600}>600 (high)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,7 @@ interface JobProgress {
|
||||
interface UseJobStatusOptions {
|
||||
onComplete?: (result: JobProgress["result"]) => void;
|
||||
onError?: (error: string) => void;
|
||||
wsBaseUrl?: string; // optional override, e.g. "wss://tools.asepharyana.my.id"
|
||||
}
|
||||
|
||||
export function useJobStatus(jobId: string | null, options?: UseJobStatusOptions) {
|
||||
@@ -40,10 +41,13 @@ export function useJobStatus(jobId: string | null, options?: UseJobStatusOptions
|
||||
const connect = useCallback(() => {
|
||||
if (!jobId) return;
|
||||
|
||||
// Connect directly to Rust gateway WebSocket (not via Next.js)
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const host = "localhost:3001"; // Rust gateway - wss for production
|
||||
const url = `${protocol}//${host}/api/job/${jobId}/ws`;
|
||||
// Determine WS base URL: from options, NEXT_PUBLIC_WS_URL, or infer from page location
|
||||
const wsBase = options?.wsBaseUrl
|
||||
?? process.env.NEXT_PUBLIC_WS_URL
|
||||
?? (typeof window !== "undefined"
|
||||
? `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}`
|
||||
: "ws://localhost:3002");
|
||||
const url = `${wsBase}/api/job/${jobId}/ws`;
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
Reference in New Issue
Block a user