diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 776c221..b6d97e2 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index 5c9ddc1..3a2ffc4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -49,7 +49,7 @@ "dayjs": "^1.11.7", "dotenv": "^16.0.3", "eslint": "8.42.0", - "eslint-config-next": "13.4.4", + "eslint-config-next": "^13.5.4", "face-api.js": "^0.22.2", "framer-motion": "^10.12.16", "graphql": "^16.6.0", @@ -61,7 +61,7 @@ "kuromoji": "^0.1.2", "kuromojin": "^3.0.0", "langchain": "^0.0.95", - "next": "13.4.9", + "next": "^13.5.4", "next-auth": "0.0.0-manual.e65faa1c", "next-contentlayer": "^0.3.2", "node-fetch": "2.6.7", @@ -69,13 +69,14 @@ "openai": "^4.5.0", "playwright-aws-lambda": "^0.10.0", "playwright-core": "^1.38.0", - "react": "18.2.0", + "react": "^18.2.0", "react-d3-cloud": "^1.0.6", - "react-dom": "18.2.0", + "react-dom": "^18.2.0", "react-hook-form": "^7.44.3", "react-icons": "^4.8.0", "react-markdown": "^8.0.7", "react-textarea-autosize": "^8.4.1", + "recharts": "^2.8.0", "reflect-metadata": "^0.1.13", "rehype-raw": "^6.1.1", "rehype-sanitize": "^5.0.1", @@ -88,7 +89,6 @@ "typegraphql-prisma": "^0.27.0", "typescript": "5.2.0-dev.20230606", "video.js": "^8.5.2", - "vidstack": "^0.6.13", "zact": "^0.0.2", "zod": "^3.21.4" }, diff --git a/frontend/src/app/_components/Chatbot.tsx b/frontend/src/app/_components/Chatbot.tsx index a6ae388..7d44e50 100644 --- a/frontend/src/app/_components/Chatbot.tsx +++ b/frontend/src/app/_components/Chatbot.tsx @@ -1,24 +1,26 @@ "use client"; import { placeholderAtom } from "@src/store/placeholder"; -import { - ArrowDownIcon, - ArrowUpIcon, - MeOutlinedIcon, -} from "@xpadev-net/designsystem-icons"; +import { Country } from "@src/types/country"; +import { ArrowDownIcon, ArrowUpIcon } from "@xpadev-net/designsystem-icons"; import { useChat } from "ai/react"; -import cn from "classnames"; import { AnimatePresence, motion } from "framer-motion"; import { useAtom } from "jotai"; import { type Session } from "next-auth"; import { useState } from "react"; import { FaMagic } from "react-icons/fa"; import { IoMdSend } from "react-icons/io"; -import { SiOpenai } from "react-icons/si"; -import ReactMarkdown from "react-markdown"; import { toast } from "sonner"; -export default function Chatbot({ user }: { user: Session["user"] }) { +import MessageItem from "../chat/Message"; + +export default function Chatbot({ + countries, + user, +}: { + countries: Country[]; + user: Session["user"]; +}) { const [isOpen, setIsOpen] = useState(false); const variants = { @@ -33,6 +35,7 @@ export default function Chatbot({ user }: { user: Session["user"] }) { }; const { + data, handleInputChange, handleSubmit, input, @@ -87,38 +90,22 @@ export default function Chatbot({ user }: { user: Session["user"] }) { >
diff --git a/frontend/src/app/api/chat/functions.ts b/frontend/src/app/api/chat/functions.ts
index c1a5d88..6d3e4b8 100644
--- a/frontend/src/app/api/chat/functions.ts
+++ b/frontend/src/app/api/chat/functions.ts
@@ -1,6 +1,15 @@
import { conn } from "@src/lib/planetscale";
import { ChatCompletionCreateParams } from "openai/resources/chat";
+export type TransformedData = {
+ country_id: string;
+ country_value: string;
+ date: string;
+ indicator_id: string;
+ indicator_value: string;
+ value: number | null;
+};
+
export const functions: ChatCompletionCreateParams.Function[] = [
{
name: "get_member_info",
@@ -89,10 +98,10 @@ async function get_population(countryCode: string) {
const data = result[1];
if (!data) {
- return "Sorry, we could not retrieve population data because there was no data available for the country code.";
+ return "申し訳ありませんが、国番号に対応するデータがないため、人口データを取得できませんでした。";
}
- const transformedData = data.map((datum: any) => {
+ const transformedData: TransformedData[] = data.map((datum: any) => {
return {
country_id: datum.country.id,
country_value: datum.country.value,
@@ -103,21 +112,21 @@ async function get_population(countryCode: string) {
};
});
+ transformedData.sort((a, b) => parseInt(a.date) - parseInt(b.date));
+
return transformedData;
} catch (e: any) {
- return `Sorry, we could not retrieve population data due to an error: ${e.message}`;
+ return `申し訳ありませんが、エラーにより人口データを取得できませんでした`;
}
}
async function get_member_info(name: string) {
- if (!conn) return null;
-
const query = "SELECT * FROM Member WHERE name = ? LIMIT 1";
const params = [name];
const data = await conn.execute(query, params);
if (data.rows.length === 0) {
- return "Sorry, the member information could not be found.";
+ return "申し訳ありませんが、議員情報が見つかりませんでした。";
}
return data.rows[0];
@@ -152,8 +161,6 @@ async function meeting_list(args: any) {
};
});
- console.log(newArray);
-
return newArray;
}
diff --git a/frontend/src/app/api/chat/route.ts b/frontend/src/app/api/chat/route.ts
index 28373f1..8bd4ad3 100644
--- a/frontend/src/app/api/chat/route.ts
+++ b/frontend/src/app/api/chat/route.ts
@@ -1,6 +1,11 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
-import { OpenAIStream, StreamingTextResponse } from "ai";
+import {
+ createStreamDataTransformer,
+ experimental_StreamData,
+ OpenAIStream,
+ StreamingTextResponse,
+} from "ai";
import { OpenAI } from "openai";
import { functions, runFunction } from "./functions";
@@ -47,41 +52,94 @@ export async function POST(req: Request) {
const { messages } = await req.json();
const key = JSON.stringify(messages);
- const cached = await await redis.get(key);
+ const cached = (await redis.get(key)) as any;
- if (cached) {
- return new Response(cached as any);
- }
+ const data = new experimental_StreamData();
+
+ if (cached && cached.completion) {
+ console.log(cached);
+
+ const chunks: string[] = [];
+ for (let i = 0; i < cached.completion.length; i += 5) {
+ chunks.push(cached.completion.substring(i, i + 5));
+ }
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ for (const chunk of chunks) {
+ const bytes = new TextEncoder().encode(chunk);
+ controller.enqueue(bytes);
+ await new Promise((r) =>
+ setTimeout(r, Math.floor(Math.random() * 40) + 10)
+ );
+ }
+ controller.close();
+ },
+ });
+
+ if (cached.data) {
+ cached.data.forEach((item: any) => {
+ data.append(item);
+ });
+ data.close();
+
+ const transformedStream = stream.pipeThrough(
+ createStreamDataTransformer(true)
+ );
+ return new StreamingTextResponse(transformedStream, {}, data);
+ } else {
+ return new StreamingTextResponse(stream);
+ }
+ }
const initialResponse = await openai.chat.completions.create({
function_call: "auto",
functions,
messages,
- model: "gpt-3.5-turbo-0613",
+ model: "gpt-3.5-turbo",
stream: true,
temperature: 0,
});
+ const allDataAppends: any[] = [];
+
const stream = OpenAIStream(initialResponse, {
experimental_onFunctionCall: async (
{ name, arguments: args },
createFunctionCallMessages
) => {
const functionResponse = await runFunction(name, args);
-
const newMessages = createFunctionCallMessages(functionResponse);
+
+ const appendData = {
+ body: functionResponse,
+ index: messages.length - 1 + 1,
+ type: typeof functionResponse === "string" ? "error" : name,
+ };
+
+ data.append(appendData);
+ allDataAppends.push(appendData);
+
return openai.chat.completions.create({
+ functions,
messages: [...messages, ...newMessages],
model: "gpt-3.5-turbo-0613",
stream: true,
+ temperature: 0,
});
},
- async onCompletion(completion) {
- // Cache the response
- await redis.set(key, completion);
+ experimental_streamData: true,
+ async onFinal(completion) {
+ data.close();
+
+ const cacheValue = {
+ completion: completion,
+ data: allDataAppends,
+ };
+ await redis.set(key, JSON.stringify(cacheValue));
await redis.expire(key, 60 * 60);
},
});
- return new StreamingTextResponse(stream);
+ return new StreamingTextResponse(stream, {}, data);
}
diff --git a/frontend/src/app/chat/Chat.tsx b/frontend/src/app/chat/Chat.tsx
index c18cbc0..291c9b2 100644
--- a/frontend/src/app/chat/Chat.tsx
+++ b/frontend/src/app/chat/Chat.tsx
@@ -2,17 +2,16 @@
import { Dialog } from "@headlessui/react";
import Modal from "@src/app/_components/Modal";
-import { MeOutlinedIcon } from "@xpadev-net/designsystem-icons";
+import { Country } from "@src/types/country";
import { useChat } from "ai/react";
-import cn from "classnames";
import { type Session } from "next-auth";
import { useEffect, useState } from "react";
import { AiOutlineArrowRight } from "react-icons/ai";
import { FaMagic } from "react-icons/fa";
-import { SiOpenai } from "react-icons/si";
-import ReactMarkdown from "react-markdown";
import { toast } from "sonner";
+import MessageItem from "./Message";
+
export function EmptyScreen({
president,
setInput,
@@ -27,7 +26,7 @@ export function EmptyScreen({
},
{
heading: `日本の人口推移の調査`,
- message: `日本の人口の推移を教えてください。またその理由を考えてください`,
+ message: `日本の人口の推移を教えてください。またその要因を考えてください`,
},
{
heading: `議会での議論を調べる`,
@@ -60,13 +59,15 @@ export function EmptyScreen({
}
export default function Chat({
+ countries,
president,
user,
}: {
+ countries: Country[];
president: string;
user: Session["user"];
}) {
- const { handleInputChange, handleSubmit, input, messages, setInput } =
+ const { data, handleInputChange, handleSubmit, input, messages, setInput } =
useChat({
api: "/api/chat",
onResponse: (response) => {
@@ -92,41 +93,25 @@ export default function Chat({
}
return (
- {label} 人口: {formatJapaneseNumber(payload[0].value)}
+ {findCountryByCode(transformedData[0].country_id)?.emoji}{" "}
+ {findCountryByCode(transformedData[0].country_id)?.name}の人口推移
+
+ 詳細情報
-