Skip to content

Storybookを追加#56

Draft
norihirosunada wants to merge 9 commits intoreceptron:mainfrom
norihirosunada:codex/implement-storybook-for-ui-components-clean
Draft

Storybookを追加#56
norihirosunada wants to merge 9 commits intoreceptron:mainfrom
norihirosunada:codex/implement-storybook-for-ui-components-clean

Conversation

@norihirosunada
Copy link
Copy Markdown
Contributor

@norihirosunada norihirosunada commented Nov 13, 2025

目的

UI開発、テストのためにStorybookを導入

スクリーンショット 2025-11-13 23 33 00

#54 #55 の後にマージ

Summary by CodeRabbit

  • New Features

    • Added automated Firebase Hosting preview deployments for pull requests with 7-day expiry.
    • Integrated Storybook for interactive component documentation and testing.
    • Implemented Vitest testing framework with component story tests.
  • Documentation

    • Added repository guidelines and setup instructions.
    • Added Firebase preview deployment operational documentation.
  • Build & Infrastructure

    • Extended build pipeline to include Storybook generation.
    • Added testing and Storybook development scripts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 13, 2025

Walkthrough

Adds Storybook configuration, Vue component stories, testing infrastructure (Vitest), mock utilities for Firebase/social auth, GitHub Actions workflow for Firebase preview deployments, repository guidelines, and substantial package.json dependency additions for GraphAI agents, Storybook, and testing tools.

Changes

Cohort / File(s) Summary
GitHub Actions & Deployment
.github/workflows/deploy-preview.yml, docs/firebase-staging-deploy.md
New GitHub Actions workflow triggered on PR events (opened, synchronize, reopened, labeled) to build and deploy to Firebase Hosting with 7-day preview channels (pr-). Includes symlink config (game-dev.ts → project.ts), nodeset skipping via 'no-preview' label, and deployment documentation covering setup, workflow details, and operational rules.
Repository Guidelines & Configuration
AGENTS.md, .gitignore, packages/grapys-vue/src/config/project.ts
New monorepo guidelines document covering project structure, build/test commands, coding conventions, and security notes. Removes AGENTS.md from .gitignore to enable git tracking. Adds symlink config stub at src/config/project.ts.
Storybook Core Infrastructure
packages/grapys-vue/.storybook/main.ts, .storybook/preview.ts, .storybook/tsconfig.json
Configures Storybook Vue 3 with Vite, tailwindcss plugin, path aliases (@ → src, config/project → game-dev.ts, Firebase/SocialLogin → mocks), autodocs, and story patterns. Wires Pinia store and global CSS in preview.
Storybook Story Files
packages/grapys-vue/src/components/*.stories.ts
New story files for Blank, Chat, Layout, Node, Node2, SideMenuButton, Signin, and SigninButton components, each with metadata, UI controls, and interactive scenarios (e.g., Firebase auth state switching for Layout, streaming messages for Chat).
Testing Infrastructure
packages/grapys-vue/vitest.config.ts, src/__tests__/storybook.spec.ts
Vitest configuration with happy-dom environment, Vue plugin, path aliases matching Storybook setup, and worker pool settings. Includes sample Vitest test validating SigninButton render.
Mock Utilities
packages/grapys-vue/src/storybook/mocks/SocialLogin.ts, .storybook/mocks/firebase.ts, .storybook/mocks/SocialLogin.ts, src/storybook/mocks/firebase.ts, src/storybook/useStoryApp.ts
Mock Firebase auth (onAuthStateChanged, triggerAuthStateChange) and Google sign-in (googleSignin callback function) for Storybook tests. Utility hook useStoryApp retrieves Vue app instance from story context.
Component Exports & Internal Changes
packages/grapys-vue/src/components/Node.ts, Node2.ts, src/agents/event.ts
Re-export default from corresponding Vue view files (Node.vue, Node2.vue). Type signature expanded in agents/event.ts to include reject property via casting.
Package Configuration
packages/grapys-vue/package.json
Build script extended to include Storybook build. New dev/test scripts: storybook, storybook:build, test:vitest, test:storybook. Added GraphAI agent dependencies (@graphai/*, @receptron/event_agent_generator, react, @mlc-ai/web-llm) and extensive dev dependencies (Storybook @8, Vitest, testing libraries, PostCSS/Tailwind).

Sequence Diagram

sequenceDiagram
    actor User
    participant GitHub
    participant Actions as GitHub Actions
    participant Firebase as Firebase Hosting
    participant PR as PR Comments

    User->>GitHub: Open/sync PR
    GitHub->>Actions: Trigger deploy-preview workflow
    
    rect rgb(200, 220, 240)
    Note over Actions: Skip if 'no-preview' label
    Actions->>Actions: Checkout code
    Actions->>Actions: Setup Node.js 20
    Actions->>Actions: Install dependencies
    Actions->>Actions: Create symlink (config)
    Actions->>Actions: Build project (Vite)
    end
    
    Actions->>Firebase: Deploy to preview channel<br/>(pr-<PR number>)
    Firebase-->>Actions: Deployment success
    
    Actions->>PR: Post/update comment<br/>with preview URL
    PR-->>User: Preview URL visible
    
    Note over Firebase: Preview expires in 7 days
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Areas requiring extra attention:

  • package.json dependencies: Verify GraphAI agent versions and react peer dependency compatibility; review bundling impact of new dependencies
  • Storybook configuration aliases: Confirm path resolution for config/project and Firebase/SocialLogin mocks aligns across Storybook, Vitest, and build tooling
  • Firebase mock implementation: Review listener registry pattern in firebase.ts for potential memory leaks or race conditions in interactive stories
  • GitHub Actions workflow: Validate Firebase secrets configuration, channel naming scheme (pr-), and 7-day expiry settings
  • Story render factories: Examine Pinia store setup consistency across Node, Node2, and Layout stories for test isolation

Poem

🐰 A fuzzy tale of preview pipelines,
Storybook tales with silken lines,
Mock Firebases and agents galore,
From game-dev.ts to stories we store—
Seven days of testing, then off they go! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Storybookを追加' (Add Storybook) clearly and concisely summarizes the main change: introducing Storybook to the repository for UI development and testing, which aligns with all the file changes shown in the raw summary.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@norihirosunada norihirosunada marked this pull request as draft November 13, 2025 14:33
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (13)
packages/grapys-vue/.storybook/tsconfig.json (1)

14-14: Consider adding common exclusions for TypeScript compilation.

The empty exclude array may result in TypeScript compiling unnecessary files like node_modules, dist, or build directories, which could slow down type checking and compilation.

Apply this diff to exclude common directories:

-  "exclude": []
+  "exclude": [
+    "../node_modules",
+    "../dist",
+    "../build"
+  ]
packages/grapys-vue/src/storybook/mocks/firebase.ts (1)

22-24: Empty object placeholders may cause runtime errors.

The firebaseApp, db, and functions exports are typed as Record<string, never>, meaning they're empty objects. If Storybook stories or tests attempt to access properties or methods on these objects, they'll receive undefined and may throw runtime errors.

Consider providing Proxy-based mocks that log warnings for unimplemented access:

-export const firebaseApp = {} as Record<string, never>;
-export const db = {} as Record<string, never>;
-export const functions = {} as Record<string, never>;
+export const firebaseApp = new Proxy({}, {
+  get(_, prop) {
+    console.warn(`[Mock] firebaseApp.${String(prop)} accessed but not implemented`);
+    return undefined;
+  }
+}) as any;
+export const db = new Proxy({}, {
+  get(_, prop) {
+    console.warn(`[Mock] db.${String(prop)} accessed but not implemented`);
+    return undefined;
+  }
+}) as any;
+export const functions = new Proxy({}, {
+  get(_, prop) {
+    console.warn(`[Mock] functions.${String(prop)} accessed but not implemented`);
+    return undefined;
+  }
+}) as any;
packages/grapys-vue/src/components/SideMenuButton.stories.ts (1)

35-55: Consider simplifying story definitions.

The Primary and Danger stories use identical render patterns. Since they only differ in args and slot content, you could omit the explicit render functions and let Storybook use the default renderer with slot text passed via args.

For example, you could use Storybook's slot handling:

 export const Primary: Story = {
   args: {
     variant: "primary",
+    default: "Primary Action",
   },
-  render: (args) => ({
-    components: { SideMenuButton },
-    setup: () => ({ args }),
-    template: '<SideMenuButton v-bind="args">Primary Action</SideMenuButton>',
-  }),
 };

However, the current explicit approach is also clear and works correctly.

packages/grapys-vue/src/__tests__/storybook.spec.ts (1)

1-15: LGTM: Good example of Storybook + Vitest integration.

This test demonstrates the pattern for testing composed Storybook stories:

  • composeStories integrates story metadata with the component
  • Testing Library provides DOM queries
  • Basic assertion confirms rendering

This pattern can be extended to test other stories and interactions.

Consider expanding test coverage to verify:

  • Different story variants (not just Default)
  • Component interactions and state changes
  • Accessibility properties
.github/workflows/deploy-preview.yml (1)

29-31: Consider build-time environment configuration as an alternative.

The symlink approach works well for switching configurations. As an alternative, you could use environment variables or build-time config injection to avoid file system manipulation in CI.

packages/grapys-vue/src/components/Node2.stories.ts (3)

29-30: Consider using structuredClone for safer deep cloning.

The JSON.parse/stringify approach works but has limitations (loses functions, Date objects, undefined values, etc.). Modern environments support structuredClone which handles more data types correctly.

Apply this diff:

-const cloneNodeData = (node: GUINodeData): GUINodeData =>
-  JSON.parse(JSON.stringify(node)) as GUINodeData;
+const cloneNodeData = (node: GUINodeData): GUINodeData =>
+  structuredClone(node);

77-77: Simplify property existence check.

The verbose Object.prototype.hasOwnProperty.call can be replaced with the in operator for better readability.

Apply this diff:

-        if (Object.prototype.hasOwnProperty.call(storyArgs, "resultUrl")) {
+        if ("resultUrl" in storyArgs) {

22-22: Avoid type assertions with as any.

Type assertions to any bypass TypeScript's type checking. Consider defining a proper interface for argTypes or using a more specific type.

packages/grapys-vue/src/components/Node.stories.ts (3)

29-30: Consider using structuredClone for safer deep cloning.

Same issue as in Node2.stories.ts - the JSON.parse/stringify approach has limitations with certain data types.


77-77: Simplify property existence check.

Same verbose syntax as in Node2.stories.ts - use the in operator instead.


22-22: Avoid type assertions with as any.

Same issue as Node2.stories.ts - define proper types for argTypes.

packages/grapys-vue/src/components/Layout.stories.ts (2)

11-28: Consider improving type safety for custom story args.

The as any type assertions on lines 24 and 27 bypass TypeScript's type checking for the custom authState control. While this pattern is common in Storybook for story-specific controls that aren't component props, it reduces type safety.

Consider defining an explicit type for the story args:

+type LayoutStoryArgs = {
+  authState: "loading" | "signedOut" | "signedIn";
+};
+
 const meta: Meta<typeof Layout> = {
   title: "Components/Layout",
   component: Layout,
   tags: ["autodocs"],
   parameters: {
     layout: "fullscreen",
   },
   argTypes: {
     authState: {
       control: "select",
       options: ["loading", "signedOut", "signedIn"],
       description: "Mocked Firebase authentication state for the story.",
     },
-  } as any,
+  } satisfies Record<keyof LayoutStoryArgs, any>,
   args: {
     authState: "loading",
-  } as any,
+  } satisfies LayoutStoryArgs,
 };

61-61: Silent error handling may hide routing issues.

The catch handler silently ignores all routing errors. While this may be intentional for the Storybook environment, it could mask real problems during story development.

Consider logging the error for debugging:

-        router.push("/").catch(() => undefined);
+        router.push("/").catch((err) => console.warn("Story router navigation failed:", err));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6177ab and c13199e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (27)
  • .github/workflows/deploy-preview.yml (1 hunks)
  • .gitignore (0 hunks)
  • AGENTS.md (1 hunks)
  • docs/firebase-staging-deploy.md (1 hunks)
  • packages/grapys-vue/.storybook/main.ts (1 hunks)
  • packages/grapys-vue/.storybook/preview.ts (1 hunks)
  • packages/grapys-vue/.storybook/tsconfig.json (1 hunks)
  • packages/grapys-vue/package.json (1 hunks)
  • packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts (1 hunks)
  • packages/grapys-vue/src/.storybook/mocks/firebase.ts (1 hunks)
  • packages/grapys-vue/src/__tests__/storybook.spec.ts (1 hunks)
  • packages/grapys-vue/src/agents/event.ts (1 hunks)
  • packages/grapys-vue/src/components/Blank.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/Chat.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/Layout.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/Node.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/Node.ts (1 hunks)
  • packages/grapys-vue/src/components/Node2.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/Node2.ts (1 hunks)
  • packages/grapys-vue/src/components/SideMenuButton.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/Signin.stories.ts (1 hunks)
  • packages/grapys-vue/src/components/SigninButton.stories.ts (1 hunks)
  • packages/grapys-vue/src/config/project.ts (1 hunks)
  • packages/grapys-vue/src/storybook/mocks/SocialLogin.ts (1 hunks)
  • packages/grapys-vue/src/storybook/mocks/firebase.ts (1 hunks)
  • packages/grapys-vue/src/storybook/useStoryApp.ts (1 hunks)
  • packages/grapys-vue/vitest.config.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • .gitignore
🧰 Additional context used
🧬 Code graph analysis (6)
packages/grapys-vue/src/components/Signin.stories.ts (1)
packages/grapys-vue/src/components/SigninButton.stories.ts (1)
  • Default (17-22)
packages/grapys-vue/src/components/Layout.stories.ts (2)
packages/grapys-vue/src/storybook/useStoryApp.ts (1)
  • useStoryApp (4-12)
packages/grapys-vue/src/store/firebase.ts (1)
  • useFirebaseStore (5-24)
packages/grapys-vue/src/components/SigninButton.stories.ts (1)
packages/grapys-vue/src/components/Signin.stories.ts (1)
  • Default (18-23)
packages/grapys-vue/src/components/Node2.stories.ts (2)
packages/grapys-vue/src/storybook/useStoryApp.ts (1)
  • useStoryApp (4-12)
packages/grapys-vue/src/store/index.ts (1)
  • useStore (19-267)
packages/grapys-vue/src/components/Node.stories.ts (2)
packages/grapys-vue/src/storybook/useStoryApp.ts (1)
  • useStoryApp (4-12)
packages/grapys-vue/src/store/index.ts (1)
  • useStore (19-267)
packages/grapys-vue/src/components/Blank.stories.ts (1)
packages/grapys-vue/src/storybook/useStoryApp.ts (1)
  • useStoryApp (4-12)
🔇 Additional comments (26)
packages/grapys-vue/src/agents/event.ts (1)

4-4: Type definition looks reasonable but verify runtime contract.

The EventWithReject type definition extending EventData with a reject method is structurally sound and follows Promise-like semantics.

However, ensure this aligns with the actual data provided by @receptron/event_agent_generator.

packages/grapys-vue/src/components/Node.ts (1)

1-1: LGTM! Clean barrel export pattern.

The re-export provides a clean interface for consumers to import the Node component, aligning with the similar pattern used in Node2.ts.

AGENTS.md (1)

1-41: LGTM! Comprehensive repository guidelines.

The documentation provides clear guidelines for the monorepo setup, covering project structure, development workflows, coding conventions, and security considerations. This is a valuable addition for team collaboration.

packages/grapys-vue/src/components/Signin.stories.ts (1)

1-24: LGTM! Standard Storybook configuration.

The story follows Storybook best practices for Vue 3, with proper type definitions, autodocs tag, and action handling configuration.

packages/grapys-vue/src/components/Blank.stories.ts (1)

40-40: Verify silent error handling is intentional.

The router navigation error is silently ignored with .catch(() => undefined). While this might be intentional for Storybook stories to prevent console noise, ensure this doesn't hide legitimate routing issues during story development.

docs/firebase-staging-deploy.md (1)

1-57: LGTM! Comprehensive Firebase preview deployment documentation.

The documentation clearly explains the preview deployment workflow, setup requirements, and operational procedures. This will be valuable for maintainers configuring the Firebase hosting integration.

packages/grapys-vue/src/storybook/mocks/firebase.ts (2)

7-14: LGTM! Solid auth state listener implementation.

The listener management using a Set provides proper subscription/unsubscription behavior and the public triggerAuthStateChange function enables effective testing of auth state changes.


1-24: Duplicate Firebase mock files do not exist.

The script output confirms there is only a single Firebase mock file at packages/grapys-vue/src/storybook/mocks/firebase.ts. The claimed duplicate at .storybook/mocks/firebase.ts does not exist. All references in both vitest.config.ts and .storybook/main.ts consistently point to the same src/storybook/mocks/ location. No maintenance issues or inconsistencies stem from duplicate mocks.

Likely an incorrect or invalid review comment.

packages/grapys-vue/src/components/SideMenuButton.stories.ts (2)

5-29: LGTM: Well-structured meta configuration.

The Storybook meta configuration is properly typed and follows Vue 3 conventions. The argTypes and default args provide a good starting point for interactive documentation.


57-66: LGTM: Disabled story properly demonstrates the disabled state.

The story correctly sets the disabled prop and provides appropriate slot content.

packages/grapys-vue/package.json (4)

8-8: Verify that Storybook should be built in every build.

The build script now includes yarn storybook:build, which couples Storybook build to the main build process. This could increase build times.

Confirm this is intentional. If Storybook is only needed for development or specific deployments, consider keeping it as a separate script.


12-15: LGTM: Testing and Storybook scripts are well-structured.

The scripts follow best practices:

  • Vitest configuration is explicit
  • Storybook test script properly uses start-server-and-test to ensure the server is running before tests
  • Output directory for Storybook build is clearly specified

38-52: LGTM: Appropriate devDependencies for Storybook and testing setup.

The devDependencies include:

  • Complete Storybook toolchain (core, addons, test-runner)
  • Vitest with Vue testing library and happy-dom
  • Build/style tooling (Vite, Tailwind, PostCSS)

All dependencies align with the PR's goal of adding Storybook for UI development.


33-33: React dependency is correctly included and intentionally required.

Verification confirms React is used in packages/grapys-vue/src/agents/event_react.ts, which imports React hooks (useState, useMemo). This is a GraphAI agent integration that requires React, making the dependency legitimate for this Vue project.

packages/grapys-vue/src/components/Node2.ts (1)

1-2: LGTM: Standard barrel export pattern.

The re-export from ../views/Node2.vue follows the existing pattern used in Node.ts and provides a clean public API for the component.

packages/grapys-vue/src/storybook/useStoryApp.ts (1)

4-12: LGTM: Well-implemented composition helper.

The useStoryApp hook correctly:

  • Uses getCurrentInstance() to access the Vue app instance
  • Validates it's called within a proper context
  • Provides a clear error message
  • Returns the app instance for use in Storybook stories

This is a clean utility for accessing the app/Pinia stores within story setup contexts.

packages/grapys-vue/src/storybook/mocks/SocialLogin.ts (1)

1-10: Consider whether errors should propagate after errorCallback.

The mock function catches errors from the callback and forwards them to errorCallback, but doesn't re-throw. This means:

  • Errors are silently swallowed after being logged to errorCallback
  • The returned function always succeeds even if callback throws

Verify whether this behavior matches the real googleSignin:

  • Should errors propagate to the caller after errorCallback handles them?
  • Or should errorCallback fully handle the error?

If errors should propagate:

   } catch (error) {
     if (error instanceof Error) {
       errorCallback?.(error);
+      throw error;
     }
   }

Otherwise, the current implementation is acceptable for Storybook mocks.

packages/grapys-vue/.storybook/preview.ts (2)

7-11: LGTM: Proper Pinia setup for Storybook.

The setup function correctly:

  • Creates a Pinia instance
  • Sets it as active with setActivePinia(pinia)
  • Registers it with the Vue app

This ensures all stories have access to Pinia stores.


13-23: LGTM: Standard Storybook preview parameters.

The preview configuration includes sensible defaults:

  • Actions auto-detection for event handlers (props starting with "on")
  • Control matchers for common types (color, date)

These provide good out-of-the-box behavior for interactive documentation.

packages/grapys-vue/src/components/SigninButton.stories.ts (1)

1-22: LGTM!

The Storybook story configuration is well-structured and follows Vue 3 best practices with proper TypeScript typing.

packages/grapys-vue/.storybook/main.ts (1)

1-42: LGTM!

The Storybook configuration properly integrates Vue 3, Vite, and TailwindCSS with well-structured path aliases for mocking Firebase and social login utilities in the story environment.

packages/grapys-vue/vitest.config.ts (1)

1-33: LGTM!

The Vitest configuration is properly set up with happy-dom for Vue component testing and maintains consistent path aliases with the Storybook configuration.

packages/grapys-vue/src/components/Chat.stories.ts (1)

1-56: LGTM!

The Chat stories effectively demonstrate different chat states including streaming messages, with realistic sample data that showcases user, assistant, and error message types.

.github/workflows/deploy-preview.yml (1)

1-48: LGTM!

The workflow properly configures Firebase preview deployments with appropriate permissions, PR-based channel naming, and resource management via 7-day expiry.

packages/grapys-vue/src/.storybook/mocks/firebase.ts (1)

1-25: LGTM!

The Firebase auth mock is clean and effective, properly implementing the listener pattern with an unsubscribe mechanism and providing a test helper for triggering auth state changes.

packages/grapys-vue/src/components/Layout.stories.ts (1)

66-79: No changes needed to the mock user properties.

The mock user with only displayName is sufficient for the Layout story. Layout.vue only accesses firebaseStore.isSignedIn (a computed Boolean), not individual User properties. Signin.vue (the only component referenced in Layout) also doesn't access User properties.

While uid is accessed elsewhere in the codebase (SideMenuSaveFirebase.vue), that component is not part of Layout's tested hierarchy in this story.

Comment on lines +1 to +10
export const googleSignin = (callback?: () => void, errorCallback?: (error: Error) => void) => () => {
try {
callback?.();
console.info("[Mock] googleSignin invoked");
} catch (error) {
if (error instanceof Error) {
errorCallback?.(error);
}
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the need for duplicate mock files.

According to the AI summary, a duplicate mock file exists at packages/grapys-vue/src/storybook/mocks/SocialLogin.ts. Consider consolidating these mocks to avoid maintenance overhead and potential inconsistencies.

Run the following script to verify the duplicate mocks:


🏁 Script executed:

#!/bin/bash
# Description: Check for duplicate SocialLogin mock files and compare their contents

fd -t f "SocialLogin.ts" packages/grapys-vue/src

echo "=== Comparing mock files ==="
diff -u packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts packages/grapys-vue/src/storybook/mocks/SocialLogin.ts || echo "Files differ or one doesn't exist"

Length of output: 137


I need to verify the exact file locations and contents. The diff produced no output, which suggests the .storybook file may not exist. Let me gather more details:


---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Verify file existence and compare mock file contents

# Check if .storybook/mocks/SocialLogin.ts exists
if [ -f "packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts" ]; then
  echo "✓ File exists: packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts"
else
  echo "✗ File does NOT exist: packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts"
fi

# Check if storybook/mocks/SocialLogin.ts exists
if [ -f "packages/grapys-vue/src/storybook/mocks/SocialLogin.ts" ]; then
  echo "✓ File exists: packages/grapys-vue/src/storybook/mocks/SocialLogin.ts"
  echo ""
  echo "=== Content of packages/grapys-vue/src/storybook/mocks/SocialLogin.ts ==="
  cat "packages/grapys-vue/src/storybook/mocks/SocialLogin.ts"
else
  echo "✗ File does NOT exist: packages/grapys-vue/src/storybook/mocks/SocialLogin.ts"
fi

# List all SocialLogin files found
echo ""
echo "=== All SocialLogin files in packages/grapys-vue/src ==="
fd -t f "SocialLogin.ts" packages/grapys-vue/src

Length of output: 658



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Compare the two mock files and show utils version

echo "=== Comparing .storybook/mocks vs storybook/mocks ==="
diff -u packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts packages/grapys-vue/src/storybook/mocks/SocialLogin.ts || true

echo ""
echo "=== Content of packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts ==="
cat packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts

echo ""
echo "=== Content of packages/grapys-vue/src/utils/firebase/SocialLogin.ts ==="
cat packages/grapys-vue/src/utils/firebase/SocialLogin.ts

Length of output: 1181


Consolidate duplicate mock files in .storybook and storybook directories.

Verification confirms two identical SocialLogin.ts mock files exist at packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts and packages/grapys-vue/src/storybook/mocks/SocialLogin.ts. Having duplicate mocks introduces maintenance overhead and risks inconsistencies if one is updated without the other. Consolidate to a single location and update all references accordingly.

🤖 Prompt for AI Agents
In packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts lines 1-10: there are
duplicate mock files at packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts
and packages/grapys-vue/src/storybook/mocks/SocialLogin.ts; consolidate by
choosing a single canonical location (prefer the .storybook path), remove the
other file, update all imports/usages across the repo to reference the chosen
path, adjust any barrel/index or tsconfig path mappings if used, and run a
global search (e.g., ripgrep/IDE) plus the build/tests to ensure no broken
imports remain.

Comment on lines +5 to +9
} catch (error) {
if (error instanceof Error) {
errorCallback?.(error);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete error handling for non-Error throw values.

The catch block only handles Error instances. If the callback throws a non-Error value (string, number, object, etc.), it won't be passed to errorCallback.

Apply this diff to handle all thrown values:

   } catch (error) {
-    if (error instanceof Error) {
-      errorCallback?.(error);
-    }
+    errorCallback?.(error instanceof Error ? error : new Error(String(error)));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
if (error instanceof Error) {
errorCallback?.(error);
}
}
} catch (error) {
errorCallback?.(error instanceof Error ? error : new Error(String(error)));
}
🤖 Prompt for AI Agents
In packages/grapys-vue/src/.storybook/mocks/SocialLogin.ts around lines 5 to 9,
the catch only forwards values that are instanceof Error; modify it to pass all
thrown values to errorCallback by calling errorCallback with the original error
when it is an Error and otherwise wrap the thrown non-Error value into a new
Error (e.g., new Error(String(error))) before passing it so
strings/numbers/objects are not dropped and the callback always receives an
Error object.

Comment on lines +9 to 12
const eventsObj = ref<Record<string, EventWithReject>>({});
const { eventAgent } = eventAgentGenerator((id, data) => {
eventsObj.value[id] = data;
eventsObj.value[id] = data as EventWithReject;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Unsafe type assertion may cause runtime errors.

The cast data as EventWithReject on line 11 asserts that the data parameter includes a reject method without verification. If @receptron/event_agent_generator provides EventData without this method, calling event.reject() in clearEvents (line 28) will throw a runtime error.

Verify that the library actually provides the reject method by checking the library's type definitions:

Consider adding a type guard or runtime check:

  const { eventAgent } = eventAgentGenerator((id, data) => {
-   eventsObj.value[id] = data as EventWithReject;
+   if ('reject' in data && typeof data.reject === 'function') {
+     eventsObj.value[id] = data as EventWithReject;
+   } else {
+     console.error('EventData missing reject method:', data);
+   }
  });

Or use optional chaining in clearEvents:

  const clearEvents = () => {
-   Object.values(eventsObj.value).forEach((event) => event.reject());
+   Object.values(eventsObj.value).forEach((event) => event.reject?.());
    eventsObj.value = {};
  };

🏁 Script executed:

#!/bin/bash
# Description: Verify EventData type definition from @receptron/event_agent_generator
# Check if reject method is part of EventData or needs to be added

# Find and examine the library's type definitions
fd -e ts -e d.ts . node_modules/@receptron/event_agent_generator --exec cat {} \; | rg -A 10 "interface EventData|type EventData"

# Also search for reject method in the library
rg -n "reject" node_modules/@receptron/event_agent_generator

Length of output: 218


I'll verify the library contract using a different approach. Let me check the package.json for the library version and search for its documentation.The web search didn't return results for the specific @receptron/event_agent_generator npm package. Let me search more directly for this package on npm.

Let me check the repository's package.json and examine how the library is used more directly.




Also, let me search the codebase to understand the actual contract:




🏁 Script executed:

#!/bin/bash
# Find package.json and check dependencies for @receptron/event_agent_generator
find . -name "package.json" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Length of output: 6225


🏁 Script executed:

#!/bin/bash
# Search for EventWithReject type definition and how it's used
rg -n "EventWithReject|EventData" --type ts -C 5

# Also check imports from the library
rg -n "@receptron/event_agent_generator" --type ts

Length of output: 15892


Based on my analysis of the codebase, I can confirm the review comment identifies a genuine and critical issue.

Key findings:

The type definition on line 4 of event.ts reveals the problem:

type EventWithReject = EventData & { reject: (reason?: any) => void };

This type intersection proves that EventData from @receptron/event_agent_generator does NOT include a reject method. The developer is explicitly adding it via intersection. The unsafe cast on line 11 (data as EventWithReject) bypasses TypeScript's type checking, masking a contract violation.

If clearEvents (referenced in the review comment at line 28) calls event.reject() without first verifying the method exists, this will throw a runtime error.

Conclusion: The original review comment is valid and warranted.

Rewritten review comment:


Fix unsafe type assertion that bypasses type safety.

Line 11 casts data to EventWithReject without runtime verification, but EventData from @receptron/event_agent_generator v0.0.6 does not include a reject method (see type definition on line 4). When clearEvents calls event.reject(), this will fail at runtime if reject is not actually present.

  • Add defensive runtime check before storing:
  const { eventAgent } = eventAgentGenerator((id, data) => {
-   eventsObj.value[id] = data as EventWithReject;
+   if ('reject' in data && typeof data.reject === 'function') {
+     eventsObj.value[id] = data as EventWithReject;
+   } else {
+     console.warn('EventData missing reject method', data);
+   }
  });

Or use optional chaining in clearEvents as defensive measure:

  Object.values(eventsObj.value).forEach((event) => event.reject?.());
🤖 Prompt for AI Agents
In packages/grapys-vue/src/agents/event.ts around lines 9 to 12, the code
unsafely casts incoming event data to EventWithReject even though EventData from
@receptron/event_agent_generator does not include reject; remove the "as
EventWithReject" assertion and instead store the value as its original EventData
type, and add a defensive runtime check so you only store/mark an event as
having a reject method if typeof data.reject === 'function' (or alternatively
keep stored events as EventData and ensure clearEvents uses optional chaining or
checks before calling event.reject()); this eliminates the unsafe cast and
prevents runtime failures when reject is absent.

Comment on lines +1 to +195
import type { Meta, StoryObj } from "@storybook/vue3";
import { action } from "@storybook/addon-actions";
import { computed, toRef, watch } from "vue";
import { createPinia, setActivePinia } from "pinia";

import Node from "./Node";
import { useStore } from "../store";
import type { GUINodeData, UpdateNodePositionData, UpdateStaticValue } from "../utils/gui/type";
import { useStoryApp } from "../storybook/useStoryApp";

const meta: Meta<typeof Node> = {
title: "Components/Node",
component: Node,
tags: ["autodocs"],
argTypes: {
nodeData: { control: false },
nearestData: { control: false },
resultUrl: {
control: "text",
description: "Optional image URL to showcase Node result rendering.",
},
} as any,
};

export default meta;

type Story = StoryObj<typeof meta>;

const cloneNodeData = (node: GUINodeData): GUINodeData =>
JSON.parse(JSON.stringify(node)) as GUINodeData;

const createRender = (): Story["render"] =>
(args) => {
const storyArgs = args as {
nodeData: GUINodeData;
nodeIndex: number;
isConnectable: boolean;
nearestData?: unknown;
resultUrl?: string;
};

return {
components: { Node },
setup() {
const app = useStoryApp();
const pinia = createPinia();
app.use(pinia);
setActivePinia(pinia);

const store = useStore();
const baseNode = cloneNodeData(storyArgs.nodeData);
store.initData([baseNode], [], { loopType: "none" });

const currentNode = computed(() => store.nodes[storyArgs.nodeIndex] ?? baseNode);

const handleUpdatePosition = (position: UpdateNodePositionData) => {
store.updateNodePosition(storyArgs.nodeIndex, position);
};
const handleSavePosition = () => {
store.saveNodePositionData();
};
const handleStaticNodeValue = (payload: Record<string, unknown>) => {
store.updateStaticNodeValue(storyArgs.nodeIndex, payload as UpdateStaticValue, true);
};
const handleNestedGraph = (payload: Record<string, unknown>) => {
store.updateNestedGraph(storyArgs.nodeIndex, payload as UpdateStaticValue);
};

const onNewEdgeStart = action("newEdgeStart");
const onNewEdge = action("newEdge");
const onNewEdgeEnd = action("newEdgeEnd");
const onOpenMenu = action("openNodeMenu");
const onOpenEditMenu = action("openNodeEditMenu");
const onDragStart = action("nodeDragStart");
const onDragEnd = action("nodeDragEnd");

if (Object.prototype.hasOwnProperty.call(storyArgs, "resultUrl")) {
const resultUrl = toRef(storyArgs, "resultUrl");
watch(
resultUrl,
(url) => {
if (url) {
store.setResult(baseNode.nodeId, { url });
return;
}
store.setResult(baseNode.nodeId, undefined);
},
{ immediate: true },
);
}

return {
args: storyArgs,
currentNode,
handleUpdatePosition,
handleSavePosition,
handleStaticNodeValue,
handleNestedGraph,
onNewEdgeStart,
onNewEdge,
onNewEdgeEnd,
onOpenMenu,
onOpenEditMenu,
onDragStart,
onDragEnd,
};
},
template: `
<div class="relative h-[360px] w-[360px] bg-slate-900 p-6">
<Node
:node-data="currentNode"
:node-index="args.nodeIndex"
:nearest-data="args.nearestData"
:is-connectable="args.isConnectable"
@update-position="handleUpdatePosition"
@save-position="handleSavePosition"
@update-static-node-value="handleStaticNodeValue"
@update-nested-graph="handleNestedGraph"
@new-edge-start="onNewEdgeStart"
@new-edge="onNewEdge"
@new-edge-end="onNewEdgeEnd"
@open-node-menu="onOpenMenu"
@open-node-edit-menu="onOpenEditMenu"
@node-drag-start="onDragStart"
@node-drag-end="onDragEnd"
/>
</div>
`,
};
};

const languageModelNode: GUINodeData = {
type: "computed",
nodeId: "assistant",
position: { x: 24, y: 24 },
data: {
guiAgentId: "openAIAgent",
params: {
system: "You are a helpful assistant.",
stream: true,
temperature: 0.7,
},
agentIndex: 0,
},
};

export const LanguageModelNode: Story = {
args: {
nodeData: languageModelNode,
nodeIndex: 0,
isConnectable: true,
},
render: createRender(),
};

const staticValueNode: GUINodeData = {
type: "static",
nodeId: "prompt",
position: { x: 24, y: 24 },
data: {
staticNodeType: "text",
value: "Tell me a story about Storybook.",
},
};

export const StaticValueNode: Story = {
args: {
nodeData: staticValueNode,
nodeIndex: 0,
isConnectable: true,
},
render: createRender(),
};

const imageNode: GUINodeData = {
type: "computed",
nodeId: "imageGenerator",
position: { x: 24, y: 24 },
data: {
guiAgentId: "openAIImageAgent",
params: {
prompt: "A neon llama visiting Storybook City",
},
},
};

export const ImageResultNode: Story = {
args: {
nodeData: imageNode,
nodeIndex: 0,
isConnectable: true,
resultUrl: "https://placekitten.com/320/200",
} as any,
render: createRender(),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract the duplicated render factory into a shared utility.

This file duplicates almost all logic from Node2.stories.ts (lines 29-130). The render factory pattern, cloneNodeData helper, and event handler setup are identical. This creates maintenance burden and violates the DRY principle.

Consider creating a shared utility in src/storybook/nodeStoryHelpers.ts:

// src/storybook/nodeStoryHelpers.ts
import type { StoryObj } from "@storybook/vue3";
import { action } from "@storybook/addon-actions";
import { computed, toRef, watch } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { useStore } from "../store";
import type { GUINodeData, UpdateNodePositionData, UpdateStaticValue } from "../utils/gui/type";
import { useStoryApp } from "./useStoryApp";

export const cloneNodeData = (node: GUINodeData): GUINodeData =>
  structuredClone(node);

export const createNodeRender = (Component: any): StoryObj["render"] =>
  (args) => {
    const storyArgs = args as {
      nodeData: GUINodeData;
      nodeIndex: number;
      isConnectable: boolean;
      nearestData?: unknown;
      resultUrl?: string;
    };

    return {
      components: { NodeComponent: Component },
      setup() {
        const app = useStoryApp();
        const pinia = createPinia();
        app.use(pinia);
        setActivePinia(pinia);

        const store = useStore();
        const baseNode = cloneNodeData(storyArgs.nodeData);
        store.initData([baseNode], [], { loopType: "none" });

        const currentNode = computed(() => store.nodes[storyArgs.nodeIndex] ?? baseNode);

        const handleUpdatePosition = (position: UpdateNodePositionData) => {
          store.updateNodePosition(storyArgs.nodeIndex, position);
        };
        const handleSavePosition = () => {
          store.saveNodePositionData();
        };
        const handleStaticNodeValue = (payload: Record<string, unknown>) => {
          store.updateStaticNodeValue(storyArgs.nodeIndex, payload as UpdateStaticValue, true);
        };
        const handleNestedGraph = (payload: Record<string, unknown>) => {
          store.updateNestedGraph(storyArgs.nodeIndex, payload as UpdateStaticValue);
        };

        const onNewEdgeStart = action("newEdgeStart");
        const onNewEdge = action("newEdge");
        const onNewEdgeEnd = action("newEdgeEnd");
        const onOpenMenu = action("openNodeMenu");
        const onOpenEditMenu = action("openNodeEditMenu");
        const onDragStart = action("nodeDragStart");
        const onDragEnd = action("nodeDragEnd");

        if ("resultUrl" in storyArgs) {
          const resultUrl = toRef(storyArgs, "resultUrl");
          watch(
            resultUrl,
            (url) => {
              if (url) {
                store.setResult(baseNode.nodeId, { url });
                return;
              }
              store.setResult(baseNode.nodeId, undefined);
            },
            { immediate: true },
          );
        }

        return {
          args: storyArgs,
          currentNode,
          handleUpdatePosition,
          handleSavePosition,
          handleStaticNodeValue,
          handleNestedGraph,
          onNewEdgeStart,
          onNewEdge,
          onNewEdgeEnd,
          onOpenMenu,
          onOpenEditMenu,
          onDragStart,
          onDragEnd,
        };
      },
      template: `
        <div class="relative h-[360px] w-[360px] bg-slate-900 p-6">
          <NodeComponent
            :node-data="currentNode"
            :node-index="args.nodeIndex"
            :nearest-data="args.nearestData"
            :is-connectable="args.isConnectable"
            @update-position="handleUpdatePosition"
            @save-position="handleSavePosition"
            @update-static-node-value="handleStaticNodeValue"
            @update-nested-graph="handleNestedGraph"
            @new-edge-start="onNewEdgeStart"
            @new-edge="onNewEdge"
            @new-edge-end="onNewEdgeEnd"
            @open-node-menu="onOpenMenu"
            @open-node-edit-menu="onOpenEditMenu"
            @node-drag-start="onDragStart"
            @node-drag-end="onDragEnd"
          />
        </div>
      `,
    };
  };

Then both story files can be simplified to:

import { createNodeRender } from "../storybook/nodeStoryHelpers";
import Node from "./Node";

// ... meta definition ...

const createRender = () => createNodeRender(Node);

// ... story definitions ...
🤖 Prompt for AI Agents
In packages/grapys-vue/src/components/Node.stories.ts lines 1-195: the render
factory, cloneNodeData helper and event handler setup are duplicated with
Node2.stories.ts; extract them into src/storybook/nodeStoryHelpers.ts as two
exports (cloneNodeData using structuredClone and createNodeRender that accepts
the component and returns the render function), move imports used by the factory
(action, vue/composables, pinia, useStore, types, useStoryApp) into that file,
then replace the local helpers in both story files by importing createNodeRender
and cloneNodeData and wire createRender = () => createNodeRender(Node); ensure
the new helper uses a generic component name (e.g., NodeComponent) and preserves
the resultUrl watch logic and all event handlers.

@@ -0,0 +1 @@
game-dev.ts No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Reconsider this symlink-based configuration pattern.

A file containing only "game-dev.ts" as a string is an unusual and fragile approach to configuration. According to the AI summary, this relies on CI creating symlinks.

This pattern has several issues:

  • Not self-documenting (the purpose is unclear from the code)
  • Fragile (breaks if symlinks aren't created)
  • Doesn't work well across different environments/OSes
  • Makes local development confusing

Consider more robust alternatives:

  1. Environment variables: Use Vite's import.meta.env for configuration
  2. Build-time configuration: Use Vite's define or mode-specific config files
  3. Direct imports: Use Vite aliases to map imports directly without symlinks

Example using environment variables:

// src/config/index.ts
const projectId = import.meta.env.VITE_PROJECT_ID || 'game-dev';
export { projectId };

Or using Vite aliases in vite.config.ts:

resolve: {
  alias: {
    '@/config/project': resolve(__dirname, `src/config/${process.env.PROJECT_NAME || 'game-dev'}.ts`)
  }
}
🤖 Prompt for AI Agents
In packages/grapys-vue/src/config/project.ts around line 1 there's a fragile
symlink-based config represented only by the string "game-dev"; replace this
with a self-contained, robust configuration: create a real config module (e.g.,
src/config/index.ts) that exports a project identifier read from
import.meta.env.VITE_PROJECT_ID with a default of "game-dev", update project
imports to reference that module, and remove the symlink-based file;
alternatively, if you prefer build-time mapping, add a Vite alias in
vite.config.ts that resolves '@/config/project' to the appropriate file based on
process.env.PROJECT_NAME, and update CI/local docs to explain using
VITE_PROJECT_ID or PROJECT_NAME for environment-specific configuration.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant