Skip to content
Open

fix #288

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,6 @@ $RECYCLE.BIN/
application/upload/**
!application/upload/**/
!application/upload/**/.gitkeep

# ローカル Lighthouse 計測の出力(任意)
scoring-tool/lighthouse*.json
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ RUN --mount=type=cache,target=/pnpm/store pnpm install --frozen-lockfile

COPY ./application .

RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
RUN NODE_OPTIONS="--max-old-space-size=4096" NODE_ENV=production pnpm build

RUN --mount=type=cache,target=/pnpm/store CI=true pnpm install --frozen-lockfile --prod --filter @web-speed-hackathon-2026/server

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ https://github.com/CyberAgentHack/web-speed-hackathon-2026/issues/new?template=a
- 例のアレ明朝: OFL 1.1 by CyberAgent, Inc.
- (Original Font) Source Han Serif JP: OFT 1.1 by Adobe http://www.adobe.com/
- Text
- 太宰治『走れメロス』(1940年)
- 太宰治『走れメロス』(1940年)
6 changes: 5 additions & 1 deletion application/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ CaX のアプリケーションコードです。

### ビルド・起動

1. アプリケーションをビルドします
1. アプリケーションをビルドします(本番相当の最適化。Docker ビルドと同じ `NODE_ENV=production` です)
- ```bash
pnpm run build
```
- 以前の「非最小化・`mode: none`」に近いビルドが必要な場合のみ(デバッグ用):
- ```bash
pnpm --filter @web-speed-hackathon-2026/client run build:dev
```
2. サーバーを起動します
- ```bash
pnpm run start
Expand Down
9 changes: 6 additions & 3 deletions application/client/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
const isProduction = process.env.NODE_ENV === "production";

module.exports = {
presets: [
["@babel/preset-typescript"],
[
"@babel/preset-env",
{
targets: "ie 11",
// 本番はレギュレーションどおり最新 Chrome 想定。modules:false で webpack が import() 分割できるようにする
targets: isProduction ? { chrome: "120" } : "ie 11",
corejs: "3",
modules: "commonjs",
modules: isProduction ? false : "commonjs",
useBuiltIns: false,
},
],
[
"@babel/preset-react",
{
development: true,
development: !isProduction,
runtime: "automatic",
},
],
Expand Down
5 changes: 4 additions & 1 deletion application/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "MPL-2.0",
"author": "CyberAgent, Inc.",
"scripts": {
"build": "NODE_ENV=development webpack",
"build": "NODE_ENV=production webpack",
"build:dev": "NODE_ENV=development webpack",
"typecheck": "tsc"
},
"dependencies": {
Expand Down Expand Up @@ -57,6 +58,7 @@
"@babel/preset-env": "7.28.3",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@tailwindcss/postcss": "4.2.2",
"@tsconfig/strictest": "2.0.8",
"@types/bluebird": "3.5.42",
"@types/common-tags": "1.8.4",
Expand All @@ -83,6 +85,7 @@
"postcss-loader": "8.2.0",
"postcss-preset-env": "10.4.0",
"react-markdown": "10.1.0",
"tailwindcss": "4.2.2",
"typescript": "5.9.3",
"webpack": "5.102.1",
"webpack-cli": "6.0.1",
Expand Down
8 changes: 1 addition & 7 deletions application/client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
const postcssImport = require("postcss-import");
const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
plugins: [
postcssImport(),
postcssPresetEnv({
stage: 3,
}),
require("@tailwindcss/postcss")(),
],
};
13 changes: 12 additions & 1 deletion application/client/src/components/application/NavigationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import classNames from "classnames";
import { MouseEventHandler, useCallback } from "react";
import { useLocation } from "react-router";

import { Link } from "@web-speed-hackathon-2026/client/src/components/foundation/Link";
import { showDialog } from "@web-speed-hackathon-2026/client/src/utils/dialog";

interface Props {
badge?: React.ReactNode;
Expand All @@ -15,10 +17,17 @@ interface Props {
export const NavigationItem = ({ badge, href, icon, command, commandfor, text }: Props) => {
const location = useLocation();
const isActive = location.pathname === href;
const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>(() => {
if (command === "show-modal" && typeof commandfor === "string") {
showDialog(commandfor);
}
}, [command, commandfor]);

return (
<li>
{href !== undefined ? (
<Link
aria-label={text}
className={classNames(
"flex flex-col items-center justify-center w-12 h-12 hover:bg-cax-brand-soft rounded-full sm:px-2 sm:w-24 sm:h-auto sm:rounded-sm lg:flex-row lg:justify-start lg:px-4 lg:py-2 lg:w-auto lg:h-auto lg:rounded-full",
{ "text-cax-brand": isActive },
Expand All @@ -33,10 +42,12 @@ export const NavigationItem = ({ badge, href, icon, command, commandfor, text }:
</Link>
) : (
<button
aria-label={text}
className="hover:bg-cax-brand-soft flex h-12 w-12 flex-col items-center justify-center rounded-full sm:h-auto sm:w-24 sm:rounded-sm sm:px-2 lg:h-auto lg:w-auto lg:flex-row lg:justify-start lg:rounded-full lg:px-4 lg:py-2"
type="button"
command={command}
commandfor={commandfor}
onClick={handleClick}
type="button"
>
<span className="relative text-xl lg:pr-2 lg:text-3xl">
{icon}
Expand Down
42 changes: 31 additions & 11 deletions application/client/src/components/application/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "@web-speed-hackathon-2026/client/src/search/services";
import { SearchFormData } from "@web-speed-hackathon-2026/client/src/search/types";
import { validate } from "@web-speed-hackathon-2026/client/src/search/validation";
import { analyzeSentiment } from "@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer";

import { Button } from "../foundation/Button";

Expand All @@ -22,6 +21,7 @@ const SearchInput = ({ input, meta }: WrappedFieldProps) => (
<div className="flex flex-1 flex-col">
<input
{...input}
aria-label="検索 (例: キーワード since:2025-01-01 until:2025-12-31)"
className={`flex-1 rounded border px-4 py-2 focus:outline-none ${
meta.touched && meta.error
? "border-cax-danger focus:border-cax-danger"
Expand Down Expand Up @@ -53,20 +53,40 @@ const SearchPageComponent = ({
}

let isMounted = true;
analyzeSentiment(parsed.keywords)
.then((result) => {
if (isMounted) {
setIsNegative(result.label === "negative");
let idleHandle: number | undefined;
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;

const run = () => {
void (async () => {
try {
const mod = await import("@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer");
const result = await mod.analyzeSentiment(parsed.keywords);
if (isMounted) {
setIsNegative(result.label === "negative");
}
} catch {
if (isMounted) {
setIsNegative(false);
}
}
})
.catch(() => {
if (isMounted) {
setIsNegative(false);
}
});
})();
};

// 検索直後の INP を落とさないよう、ネガポジ判定はアイドル時に回す
if (typeof requestIdleCallback !== "undefined") {
idleHandle = requestIdleCallback(run, { timeout: 2500 });
} else {
timeoutHandle = setTimeout(run, 1);
}

return () => {
isMounted = false;
if (idleHandle !== undefined && typeof cancelIdleCallback !== "undefined") {
cancelIdleCallback(idleHandle);
}
if (timeoutHandle !== undefined) {
clearTimeout(timeoutHandle);
}
};
}, [parsed.keywords]);

Expand Down
36 changes: 17 additions & 19 deletions application/client/src/components/crok/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import "katex/dist/katex.min.css";
import Markdown from "react-markdown";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";

import { CodeBlock } from "@web-speed-hackathon-2026/client/src/components/crok/CodeBlock";
import { SimpleMarkdown } from "@web-speed-hackathon-2026/client/src/components/crok/SimpleMarkdown";
import { TypingIndicator } from "@web-speed-hackathon-2026/client/src/components/crok/TypingIndicator";
import { CrokLogo } from "@web-speed-hackathon-2026/client/src/components/foundation/CrokLogo";

interface Props {
isStreaming?: boolean;
message: Models.ChatMessage;
}

Expand All @@ -22,24 +17,27 @@ const UserMessage = ({ content }: { content: string }) => {
);
};

const AssistantMessage = ({ content }: { content: string }) => {
const AssistantMessage = ({
content,
isStreaming = false,
}: {
content: string;
isStreaming?: boolean;
}) => {
return (
<div className="mb-6 flex gap-4">
<div className="h-8 w-8 shrink-0">
<CrokLogo className="h-full w-full" />
</div>
<div className="min-w-0 flex-1">
<div className="text-cax-text mb-1 text-sm font-medium">Crok</div>
<div className="markdown text-cax-text max-w-none">
<div className="text-cax-text max-w-none">
{content ? (
<Markdown
components={{ pre: CodeBlock }}
key={content}
rehypePlugins={[rehypeKatex]}
remarkPlugins={[remarkMath, remarkGfm]}
>
{content}
</Markdown>
isStreaming ? (
<p className="mb-4 whitespace-pre-wrap leading-relaxed">{content}</p>
) : (
<SimpleMarkdown content={content} />
)
) : (
<TypingIndicator />
)}
Expand All @@ -49,9 +47,9 @@ const AssistantMessage = ({ content }: { content: string }) => {
);
};

export const ChatMessage = ({ message }: Props) => {
export const ChatMessage = ({ isStreaming = false, message }: Props) => {
if (message.role === "user") {
return <UserMessage content={message.content} />;
}
return <AssistantMessage content={message.content} />;
return <AssistantMessage content={message.content} isStreaming={isStreaming} />;
};
37 changes: 23 additions & 14 deletions application/client/src/components/crok/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ComponentProps, isValidElement, ReactElement, ReactNode } from "react";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { ComponentProps, isValidElement, lazy, ReactElement, ReactNode, Suspense } from "react";

const LazySyntaxHighlighter = lazy(() =>
import("@web-speed-hackathon-2026/client/src/components/crok/SyntaxHighlighterLoader"),
);

const getLanguage = (children: ReactElement<ComponentProps<"code">>) => {
const className = children.props.className;
Expand All @@ -20,17 +22,24 @@ export const CodeBlock = ({ children }: ComponentProps<"pre">) => {
const code = children.props.children?.toString() ?? "";

return (
<SyntaxHighlighter
customStyle={{
fontSize: "14px",
padding: "24px 16px",
borderRadius: "8px",
border: "1px solid var(--color-cax-border)",
}}
language={language}
style={atomOneLight}
<Suspense
fallback={
<pre className="border-cax-border bg-cax-surface-subtle overflow-x-auto rounded-lg border px-4 py-6 text-sm">
<code>{code}</code>
</pre>
}
>
{code}
</SyntaxHighlighter>
<LazySyntaxHighlighter
customStyle={{
fontSize: "14px",
padding: "24px 16px",
borderRadius: "8px",
border: "1px solid var(--color-cax-border)",
}}
language={language}
>
{code}
</LazySyntaxHighlighter>
</Suspense>
);
};
5 changes: 4 additions & 1 deletion application/client/src/components/crok/CrokGate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet";

import { showDialog } from "@web-speed-hackathon-2026/client/src/utils/dialog";

interface Props {
headline: string;
description?: string;
Expand All @@ -23,9 +25,10 @@ export const CrokGate = ({
{description !== "" ? <p className="text-cax-text-muted text-sm">{description}</p> : null}
<button
className="bg-cax-brand text-cax-surface-raised hover:bg-cax-brand-strong inline-flex items-center justify-center rounded-full px-6 py-2 shadow"
type="button"
command="show-modal"
commandfor={authModalId}
onClick={() => showDialog(authModalId)}
type="button"
>
{buttonLabel}
</button>
Expand Down
6 changes: 5 additions & 1 deletion application/client/src/components/crok/CrokPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export const CrokPage = ({ messages, isStreaming, onSendMessage }: Props) => {
{messages.length === 0 && <WelcomeScreen />}

{messages.map((message, index) => (
<ChatMessage key={index} message={message} />
<ChatMessage
isStreaming={isStreaming && index === messages.length - 1 && message.role === "assistant"}
key={index}
message={message}
/>
))}
<div ref={messagesEndRef} />
</div>
Expand Down
Loading
Loading