这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@Sunwood-ai-labs
Copy link
Owner

Summary

  • scaffold a Next.js 14 + Tailwind project that hosts the Tilely collage editor experience
  • implement asset management, canvas preview, property controls, timeline trimming, and export simulation per spec
  • add shared shadcn-style UI primitives, Zustand store, presets, and refreshed README documentation

Testing

  • not run (not available in container)

https://chatgpt.com/codex/tasks/task_e_68fe18bff25c832caddc0ac336d635cd

@Sunwood-ai-labs Sunwood-ai-labs merged commit 5969b49 into main Oct 26, 2025
0 of 4 checks passed
@Sunwood-ai-labs Sunwood-ai-labs deleted the codex/create-tilely-web-app-implementation branch October 26, 2025 13:04
@gemini-code-assist
Copy link

Summary of Changes

Hello @Sunwood-ai-labs, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes the initial scaffolding and core features for 'Tilely,' a new Next.js 14-based web studio designed for creating dynamic video and image collages. It provides a robust platform for users to manage their media assets, arrange them on a customizable canvas with various layout and style options, and fine-tune individual media properties and timelines. The new studio aims to offer a seamless and intuitive experience for collage creation, from asset import to simulated export, leveraging a modern and efficient frontend architecture.

Highlights

  • New Feature: Tilely Collage Editor: Introduces a full-fledged Next.js 14 studio for creating video and image collages, providing a dedicated environment for creative media arrangement.
  • Core Editor Functionality: Implements essential editing features including asset management, a dynamic canvas preview, comprehensive property controls for individual cells, and a timeline for precise media trimming.
  • Export Simulation: Includes a simulated export process, allowing users to queue and monitor rendering jobs, providing feedback on the export status and a placeholder output URL.
  • Modern Frontend Stack: The project is built with a contemporary technology stack, utilizing Next.js 14 (App Router), Tailwind CSS, shadcn/ui for reusable components, and Zustand for efficient state management.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

There is a problem with the Gemini CLI PR review. Please check the action logs for details.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

このプルリクエストは、多機能なコラージュエディタである "Tilely" Next.js アプリケーションを導入するものです。UI、Zustand を用いた状態管理、様々なエディタ機能など、実装は包括的です。コードはよく構造化されており、モダンな React のプラクティスに従っています。

レビューを通して、いくつかの改善点を特定しました:

  • editor-shell.tsx において、クリアされないタイムアウトによる潜在的なメモリリーク。
  • top-bar.tsx において、より堅牢にできるハードコードされた値や壊れやすいロジック。
  • canvas-preview.tsx におけるパフォーマンス最適化の機会。
  • lib/store.ts において、Promise ベースのファイル処理でエラーがハンドルされておらず、UIがハングする可能性がある重大な問題。

全体として、これは Tilely アプリケーションの素晴らしい基盤です。これらの点を修正することで、アプリケーションの安定性と保守性がさらに向上するでしょう。

Comment on lines +291 to +327
export async function fileToAsset(file: File, type: AssetType): Promise<Asset> {
const url = URL.createObjectURL(file);
const baseAsset: Asset = {
id: uuid(),
name: file.name,
type,
url,
size: file.size,
createdAt: Date.now()
};

if (type === "image" || type === "logo") {
const dimensions = await new Promise<{ width: number; height: number }>((resolve) => {
const image = new Image();
image.onload = () => resolve({ width: image.width, height: image.height });
image.src = url;
});
return { ...baseAsset, ...dimensions };
}

if (type === "video" || type === "audio") {
const metadata = await new Promise<{ duration: number; width?: number; height?: number }>((resolve) => {
const element = document.createElement(type === "audio" ? "audio" : "video");
element.preload = "metadata";
element.onloadedmetadata = () =>
resolve({
duration: element.duration,
width: element instanceof HTMLVideoElement ? element.videoWidth : undefined,
height: element instanceof HTMLVideoElement ? element.videoHeight : undefined
});
element.src = url;
});
return { ...baseAsset, ...metadata };
}

return baseAsset;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

fileToAsset 関数内の new Promise で、アセットの読み込みに失敗した場合のエラーが処理されていません。例えば、ファイルが壊れている場合、onerror が呼ばれますが、Promise は reject されずに永遠に pending 状態になります。これにより、UIが応答しなくなる可能性があります。
image.onerrorelement.onerror ハンドラを追加して Promise を reject するように修正してください。

export async function fileToAsset(file: File, type: AssetType): Promise<Asset> {
  const url = URL.createObjectURL(file);
  const baseAsset: Asset = {
    id: uuid(),
    name: file.name,
    type,
    url,
    size: file.size,
    createdAt: Date.now()
  };

  if (type === "image" || type === "logo") {
    const dimensions = await new Promise<{ width: number; height: number }>((resolve, reject) => {
      const image = new Image();
      image.onload = () => resolve({ width: image.width, height: image.height });
      image.onerror = reject;
      image.src = url;
    });
    return { ...baseAsset, ...dimensions };
  }

  if (type === "video" || type === "audio") {
    const metadata = await new Promise<{ duration: number; width?: number; height?: number }>((resolve, reject) => {
      const element = document.createElement(type === "audio" ? "audio" : "video");
      element.preload = "metadata";
      element.onloadedmetadata = () =>
        resolve({
          duration: element.duration,
          width: element instanceof HTMLVideoElement ? element.videoWidth : undefined,
          height: element instanceof HTMLVideoElement ? element.videoHeight : undefined
        });
      element.onerror = reject;
      element.src = url;
    });
    return { ...baseAsset, ...metadata };
  }

  return baseAsset;
}

Comment on lines +23 to +47
useEffect(() => {
if (!renderJob || (renderJob.status !== "queued" && renderJob.status !== "processing")) {
return;
}
let progress = renderJob.progress;
let frame = 0;
updateRenderProgress(progress, "processing");
const interval = window.setInterval(() => {
frame += 1;
progress = Math.min(100, progress + 12);
if (progress >= 100) {
const blob = new Blob(["Tilely export placeholder"], { type: "text/plain" });
const url = URL.createObjectURL(blob);
updateRenderProgress(100, "succeeded", url);
window.setTimeout(() => URL.revokeObjectURL(url), 10_000);
window.clearInterval(interval);
} else {
updateRenderProgress(progress, "processing");
}
if (frame > 20) {
window.clearInterval(interval);
}
}, 500);
return () => window.clearInterval(interval);
}, [renderJob?.id]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useEffect 内で window.setTimeout を使用していますが、クリーンアップ関数で clearTimeout が呼ばれていません。コンポーネントがアンマウントされた場合にメモリリークや意図しない動作を引き起こす可能性があります。
timeoutId を変数に保持し、クリーンアップ関数でクリアすることをお勧めします。また、useEffect の依存配列に updateRenderProgress を追加すると、より堅牢になります。

  useEffect(() => {
    if (!renderJob || (renderJob.status !== "queued" && renderJob.status !== "processing")) {
      return;
    }
    let progress = renderJob.progress;
    let frame = 0;
    updateRenderProgress(progress, "processing");
    let timeoutId: ReturnType<typeof setTimeout>;
    const interval = window.setInterval(() => {
      frame += 1;
      progress = Math.min(100, progress + 12);
      if (progress >= 100) {
        const blob = new Blob(["Tilely export placeholder"], { type: "text/plain" });
        const url = URL.createObjectURL(blob);
        updateRenderProgress(100, "succeeded", url);
        timeoutId = window.setTimeout(() => URL.revokeObjectURL(url), 10_000);
        window.clearInterval(interval);
      } else {
        updateRenderProgress(progress, "processing");
      }
      if (frame > 20) {
        window.clearInterval(interval);
      }
    }, 500);
    return () => {
      window.clearInterval(interval);
      if (timeoutId) {
        window.clearTimeout(timeoutId);
      }
    };
  }, [renderJob?.id, updateRenderProgress]);

<Button
className="w-full"
onClick={() => {
queueRender("custom", customResolution * customFps > 40000 ? "server" : "browser");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

マジックナンバー 40000 が使われています。これはコードの可読性と保守性を低下させる可能性があります。
この値が何を表すのかを示す名前付き定数を定義することをお勧めします。

const BROWSER_RENDER_PIXEL_LIMIT = 40000; // 例: 解像度 * FPS の上限
// ...
queueRender("custom", customResolution * customFps > BROWSER_RENDER_PIXEL_LIMIT ? "server" : "browser");

target: "browser" | "server",
queueRender: (presetId: string, target: "browser" | "server") => void
) {
const preset = exportPresets[target === "browser" ? 0 : 1];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

exportPresets 配列のインデックス (0 or 1) に直接依存してプリセットを選択しています。これは将来 exportPresets の順序が変更された場合に壊れやすい実装です。
id や他のプロパティでプリセットを検索する方法がより堅牢です。

// 例
const presetId = target === "browser" ? "social-1080p" : "hq-2160p";
const preset = exportPresets.find(p => p.id === presetId);
if (!preset) {
  toast.error("書き出しプリセットが見つかりませんでした。");
  return;
}
queueRender(preset.id, target);
toast.success(`${preset.label} でレンダリング開始したよ〜!`);

Comment on lines +54 to +78
{cells.map((cell, index) => {
const track = project.tracks.find((item) => item.cellIndex === index);
const asset = track ? project.assets.find((item) => item.id === track.assetId) : undefined;
const isActive = activeCell === index;
return (
<button
key={cell.id}
type="button"
onClick={() => setActiveCell(index)}
className={cn(
"relative flex h-full w-full items-center justify-center overflow-hidden rounded-[inherit] border border-transparent transition",
isActive ? "border-indigo-400 shadow-[0_0_0_2px_rgba(129,140,248,0.6)]" : "border-white/5"
)}
>
{asset ? (
<AssetPreview trackId={track!.id} />
) : (
<div className="flex flex-col items-center gap-1 text-xs text-muted-foreground">
<ImageOff className="h-5 w-5" />
セル {index + 1}
</div>
)}
</button>
);
})}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

cells.map の中で project.tracks.find を使用しているため、セルごとにトラック配列全体を走査しています。セルの数やトラック数が多くなるとパフォーマンスに影響を与える可能性があります。
useMemo を使って cellIndex からトラックへのマップを事前に作成し、O(1) でアクセスできるようにすることで、この処理を最適化できます。

import { type Track } from "@/lib/types";

export function CanvasPreview() {
  // ...
  const tracksByCellIndex = useMemo(() => {
    return project.tracks.reduce((map, track) => {
      map.set(track.cellIndex, track);
      return map;
    }, new Map<number, Track>());
  }, [project.tracks]);

  // ...
  // .map() の中で
  const track = tracksByCellIndex.get(index);
  // ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants