Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions app/(main)/drive/folder/[folderId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { redirect } from "next/navigation";
import { listFoldersByParent, listFilesByParent, getFolderNameById, getFolderBreadcrumb, isGoogleDriveFolder } from "@/lib/modules/drive";
import { FilesSearchBar } from "@/components/drive/FilesSearchBar";
import { FilesResultsTable } from "@/components/drive/FilesResultsTable";
import { FilesResultsTableMobile } from "@/components/drive/FilesResultsTableMobile";
import { DriveMobileHeader } from "@/components/drive/DriveMobileHeader";
import { MobileDriveFab } from "@/components/drive/MobileDriveFab";

interface PageProps {
params: Promise<{ folderId: string }>
Expand All @@ -23,16 +26,32 @@ export default async function FolderPage({ params }: PageProps) {
const entries = [...folders, ...files]

return (
<div className="space-y-6">
<FilesSearchBar />
<>
{/* Mobile header: fixed search + filters */}
<DriveMobileHeader />
{/* Spacer to offset the fixed mobile header height */}
<div className="md:hidden h-[136px]" />

{/* Mobile results list (full-width, scrolls under header) */}
<div className="md:hidden">
<FilesResultsTableMobile entries={entries} parentName={parentName ?? undefined} />
</div>

{/* Mobile floating action button */}
<MobileDriveFab parentId={folderId} />

{/* Desktop layout */}
<div className="hidden md:block space-y-6">
<FilesSearchBar />
<FilesResultsTable
entries={entries}
parentName={parentName ?? undefined}
parentId={folderId}
breadcrumb={breadcrumb}
isGoogleDriveFolder={isDrive}
/>
</div>
</div>
</>
);
}

Expand Down
8 changes: 6 additions & 2 deletions app/(main)/drive/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FilesLeftSidebar } from "@/components/drive/FilesLeftSidebar"
import { auth } from "@/lib/auth"
import { redirect } from "next/navigation"
import { findLocalRootFolderId, getGoogleRootFolderId } from "@/lib/modules/drive"
import { DriveBottomNav } from "@/components/drive/DriveBottomNav"

export default async function DriveLayout({ children }: { children: React.ReactNode }) {
const session = await auth()
Expand All @@ -15,10 +16,13 @@ export default async function DriveLayout({ children }: { children: React.ReactN

return (
<div className="flex w-full">
<FilesLeftSidebar localRootId={localRootId} googleRootId={googleRootId} />
<main className="flex-1">
<div className="hidden md:block">
<FilesLeftSidebar localRootId={localRootId} googleRootId={googleRootId} />
</div>
<main className="flex-1 pb-16 md:pb-0">
{children}
</main>
<DriveBottomNav localRootId={localRootId} />
</div>
)
}
Expand Down
27 changes: 23 additions & 4 deletions app/(main)/drive/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { redirect } from "next/navigation";
import { getRootFolderId, listFoldersByParent, listFilesByParent, getFolderBreadcrumb, isGoogleDriveFolder } from "@/lib/modules/drive";
import { FilesSearchBar } from "@/components/drive/FilesSearchBar";
import { FilesResultsTable } from "@/components/drive/FilesResultsTable";
import { FilesResultsTableMobile } from "@/components/drive/FilesResultsTableMobile";
import { DriveMobileHeader } from "@/components/drive/DriveMobileHeader";
import { MobileDriveFab } from "@/components/drive/MobileDriveFab";

interface FilesPageProps {
searchParams?: Promise<{ parentId?: string | string[] }>
Expand All @@ -29,10 +32,26 @@ export default async function FilesPage({ searchParams }: FilesPageProps) {
const entries = [...folders, ...files]

return (
<div className="space-y-6">
<FilesSearchBar />
<FilesResultsTable entries={entries} parentId={effectiveRootId} breadcrumb={breadcrumb} isGoogleDriveFolder={isDrive} />
</div>
<>
{/* Mobile header: fixed search + filters */}
<DriveMobileHeader />
{/* Spacer to offset the fixed mobile header height */}
<div className="md:hidden h-[136px]" />

{/* Mobile results list (full-width, scrolls under header) */}
<div className="md:hidden">
<FilesResultsTableMobile entries={entries} parentName={breadcrumb?.[breadcrumb.length - 1]?.name} />
</div>

{/* Mobile floating action button */}
<MobileDriveFab parentId={effectiveRootId} />

{/* Desktop layout */}
<div className="hidden md:block space-y-6">
<FilesSearchBar />
<FilesResultsTable entries={entries} parentId={effectiveRootId} breadcrumb={breadcrumb} isGoogleDriveFolder={isDrive} />
</div>
</>
);
}

Expand Down
30 changes: 25 additions & 5 deletions app/(main)/drive/starred/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,39 @@ import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { FilesSearchBar } from "@/components/drive/FilesSearchBar";
import { FilesResultsTable } from "@/components/drive/FilesResultsTable";
import { listStarredEntries } from "@/lib/modules/drive";
import { listStarredEntries, getRootFolderId } from "@/lib/modules/drive";
import { FilesResultsTableMobile } from "@/components/drive/FilesResultsTableMobile";
import { DriveMobileHeader } from "@/components/drive/DriveMobileHeader";
import { MobileDriveFab } from "@/components/drive/MobileDriveFab";

export default async function StarredPage() {
const session = await auth()
if (!session?.user?.id) redirect('/login')

const entries = await listStarredEntries(session.user.id)
const rootId = await getRootFolderId(session.user.id)

return (
<div className="space-y-6">
<FilesSearchBar />
<FilesResultsTable entries={entries} parentName={"Starred"} />
</div>
<>
{/* Mobile header: fixed search + filters */}
<DriveMobileHeader />
{/* Spacer to offset the fixed mobile header height */}
<div className="md:hidden h-[136px]" />

{/* Mobile results list (full-width, scrolls under header) */}
<div className="md:hidden">
<FilesResultsTableMobile entries={entries} parentName={"Starred"} />
</div>

{/* Mobile floating action button */}
<MobileDriveFab parentId={rootId} />

{/* Desktop layout */}
<div className="hidden md:block space-y-6">
<FilesSearchBar />
<FilesResultsTable entries={entries} parentName={"Starred"} />
</div>
</>
)
}

Expand Down
27 changes: 23 additions & 4 deletions app/(main)/drive/trash/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { redirect } from "next/navigation";
import { getTrashFolderId, listFoldersByParent, listFilesByParent } from "@/lib/modules/drive";
import { FilesResultsTable } from "@/components/drive/FilesResultsTable";
import { FilesSearchBar } from "@/components/drive/FilesSearchBar";
import { FilesResultsTableMobile } from "@/components/drive/FilesResultsTableMobile";
import { DriveMobileHeader } from "@/components/drive/DriveMobileHeader";
import { MobileDriveFab } from "@/components/drive/MobileDriveFab";

export default async function TrashPage() {
const session = await auth();
Expand All @@ -20,10 +23,26 @@ export default async function TrashPage() {
const breadcrumb = [{ id: trashId, name: 'Trash' }]

return (
<div className="space-y-6">
<FilesSearchBar />
<FilesResultsTable entries={entries} parentId={trashId} parentName="Trash" breadcrumb={breadcrumb} />
</div>
<>
{/* Mobile header: fixed search + filters */}
<DriveMobileHeader />
{/* Spacer to offset the fixed mobile header height */}
<div className="md:hidden h-[136px]" />

{/* Mobile results list (full-width, scrolls under header) */}
<div className="md:hidden">
<FilesResultsTableMobile entries={entries} parentName="Trash" />
</div>

{/* Mobile floating action button */}
<MobileDriveFab parentId={trashId} isTrash />

{/* Desktop layout */}
<div className="hidden md:block space-y-6">
<FilesSearchBar />
<FilesResultsTable entries={entries} parentId={trashId} parentName="Trash" breadcrumb={breadcrumb} />
</div>
</>
);
}

Expand Down
47 changes: 46 additions & 1 deletion app/api/v1/videos/sora2/[id]/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { ProviderService } from '@/lib/modules/ai/providers/provider.service'
import db from '@/lib/db'
import OpenAI from 'openai'

export const runtime = 'nodejs'
Expand Down Expand Up @@ -85,7 +86,51 @@ export async function GET(_request: NextRequest, context: { params: Promise<{ id
const status: string = (anyJson?.status || '').toString()
const progress: number = Number.isFinite(anyJson?.progress) ? Number(anyJson.progress) : 0

return NextResponse.json({ status, progress, job: json })
// If we've already saved the asset locally, include the local URL
let url: string | null = null
let fileId: string | null = null
try {
type Row = { id: string; filename: string; path: string | null; parentId: string | null }
let rows: Row[] = []
try {
rows = await db.$queryRaw<Row[]>`
SELECT id, filename, path, parent_id as parentId
FROM "file"
WHERE user_id = ${userId}
AND COALESCE(CAST(json_extract(meta, '$.jobId') AS TEXT), '') = ${videoId}
LIMIT 1`
} catch {
rows = await db.$queryRaw<Row[]>`
SELECT id, filename, path, parent_id as parentId
FROM "file"
WHERE user_id = ${userId}
AND COALESCE((meta ->> 'jobId')::text, '') = ${videoId}
LIMIT 1`
}
const file = rows && rows[0]
if (file) {
let rel = ''
const p = file.path ? String(file.path) : ''
if (p) {
let normalized = p.replace(/^\/+/, '')
if (!normalized.endsWith('/' + file.filename)) {
normalized = normalized + '/' + file.filename
}
if (normalized.startsWith('data/files/')) {
normalized = normalized.slice('data/'.length)
}
rel = normalized
} else if (file.parentId) {
rel = `files/${file.parentId}/${file.filename}`
} else {
rel = `files/${file.filename}`
}
url = rel.startsWith('files/') ? `/${rel}` : `/files/${rel}`
fileId = file.id
}
} catch {}

return NextResponse.json({ status, progress, job: json, ...(url ? { url, fileId } : {}) })
} catch (error) {
return NextResponse.json({ error: 'Failed to check status' }, { status: 500 })
}
Expand Down
50 changes: 45 additions & 5 deletions app/files/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { readFile, stat } from 'fs/promises'
import { createReadStream } from 'fs'
import path from 'path'
import { LOCAL_BASE_DIR } from '@/lib/modules/drive/providers/local.service'

Expand All @@ -20,6 +21,7 @@ function resolveSafePath(segments: string[]): string | null {

// Next.js dynamic API params are async; await them before use
export async function GET(_req: Request, context: { params: Promise<{ path?: string[] }> }) {
const req = _req
const { path: rawSegments } = await context.params
if (!rawSegments || rawSegments.length === 0) {
return new Response('Not Found', { status: 404 })
Expand Down Expand Up @@ -48,7 +50,6 @@ export async function GET(_req: Request, context: { params: Promise<{ path?: str
try {
const s = await stat(cand)
if (!s.isFile()) continue
const data = await readFile(cand)
const ext = cand.toLowerCase().split('.').pop() || ''
const type =
ext === 'png' ? 'image/png' :
Expand All @@ -63,13 +64,52 @@ export async function GET(_req: Request, context: { params: Promise<{ path?: str
ext === 'avif' ? 'image/avif' :
ext === 'pdf' ? 'application/pdf' :
ext === 'txt' ? 'text/plain; charset=utf-8' :
ext === 'mp4' || ext === 'm4v' ? 'video/mp4' :
ext === 'mov' ? 'video/quicktime' :
ext === 'webm' ? 'video/webm' :
ext === 'ogv' || ext === 'ogg' ? 'video/ogg' :
ext === 'mkv' ? 'video/x-matroska' :
'application/octet-stream'
return new Response(new Uint8Array(data), {

const range = req.headers.get('range')
const fileSize = s.size
const commonHeaders: Record<string, string> = {
'content-type': type,
'accept-ranges': 'bytes',
'cache-control': 'no-store',
}

if (range) {
// Parse Range: bytes=start-end
const match = /bytes=(\d+)-(\d+)?/.exec(range)
if (!match) {
return new Response('Invalid Range', { status: 416 })
}
const start = parseInt(match[1]!, 10)
const end = match[2] ? Math.min(parseInt(match[2]!, 10), fileSize - 1) : Math.min(start + 1024 * 1024 * 4 - 1, fileSize - 1) // 4MB default chunk
if (isNaN(start) || isNaN(end) || start > end || start >= fileSize) {
return new Response('Invalid Range', { status: 416 })
}
const chunkSize = end - start + 1
const stream = createReadStream(cand, { start, end })
return new Response(stream as any, {
status: 206,
headers: {
...commonHeaders,
'content-length': String(chunkSize),
'content-range': `bytes ${start}-${end}/${fileSize}`,
}
})
}

// No range: stream entire file
const fullStream = createReadStream(cand)
return new Response(fullStream as any, {
status: 200,
headers: {
'content-type': type,
'cache-control': 'no-store',
...(ext === 'pdf' ? { 'content-disposition': `inline; filename="${last}"` } : {}),
...commonHeaders,
'content-length': String(fileSize),
...(ext === 'pdf' ? { 'content-disposition': `inline; filename="${last}"` } : {}),
}
})
} catch {
Expand Down
4 changes: 4 additions & 0 deletions components/ai/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export function VideoJob({ jobId }: VideoJobProps) {
if (cancelled) return
setStatus(String(data.status || 'queued'))
setProgress(Number.isFinite(data.progress) ? Number(data.progress) : 0)
if (typeof data.url === 'string' && data.url) {
setUrl(data.url)
return
}
// Upstream moderation or provider errors (surface and stop polling)
const jobError = (data?.job && typeof data.job === 'object') ? (data.job as any).error : null
if (jobError && (jobError.message || jobError.code)) {
Expand Down
9 changes: 7 additions & 2 deletions components/chat/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,15 +370,20 @@ export default function ChatMessages({
if (latestVideoPart) {
const out = latestVideoPart?.output
const url: string | undefined = (out && typeof out.url === 'string' && out.url) ? out.url : undefined
if (url) {
const jobId: string | undefined = (out?.details?.job?.id as string) || undefined
// Prefer local files; ignore remote API URLs
if (url && url.startsWith('/')) {
return (
<div className="mb-3 rounded-lg overflow-hidden border max-w-[1024px]">
<video src={url} controls className="w-full h-auto" />
</div>
)
}
// If the tool provided a remote URL, fall back to VideoJob to resolve the local saved asset
if (url && jobId) {
return <VideoJob jobId={jobId} />
}
// Poll job status while queued
const jobId: string | undefined = (out?.details?.job?.id as string) || undefined
if (jobId) {
return <VideoJob jobId={jobId} />
}
Expand Down
Loading
Loading