diff --git a/app/_components/common/Player.tsx b/app/_components/common/Player.tsx
new file mode 100644
index 0000000..c7b2880
--- /dev/null
+++ b/app/_components/common/Player.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import { usePlayer } from '@/app/_hooks/usePlayer';
+import { secondsToMinutes } from '@/app/_lib/second-to-minutes';
+import { cx } from 'class-variance-authority';
+import clsx from 'clsx';
+import { Maximize2, Minimize2, Pause, Play, Volume2, VolumeX } from 'lucide-react';
+import dynamic from 'next/dynamic'
+import { ReactPlayerProps } from 'react-player'
+
+const ReactPlayer = dynamic(() => import('react-player'), { ssr: false })
+
+interface PlayerProps extends ReactPlayerProps {
+ aspect?: "square" | "video"
+}
+
+export default function Player({ aspect = "video", playing: initialPlaying, muted: initialMuted, ...props }: PlayerProps) {
+ const { containerRef, playerRef, playing, muted, played, duration, isFullScreen, handleProgress, handleDuration, handlePlayPause, handleMuted, handleSeekChange, handleSeekMouseDown, handleSeekMouseUp, handleFullscreen } = usePlayer({ playing: initialPlaying, muted: initialMuted });
+
+ const buttonStyle = cx("w-11 h-11 flex items-center justify-center rounded-full bg-[#67675780] outline-[0.8px] outline-[#FFFFFF] -outline-offset-[3px] hover:outline-[#06474C] text-[#FFFFFF] duration-200 transition-all hover:bg-[#FFFFFF] hover:text-[#06474C] cursor-pointer")
+
+ return (
+
+
+
+
+
+
+
+
+ {secondsToMinutes((played * duration).toFixed(1))} / {secondsToMinutes(duration.toFixed(1))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/_components/home/Welcome.tsx b/app/_components/home/Welcome.tsx
index 6e5ce92..773123d 100644
--- a/app/_components/home/Welcome.tsx
+++ b/app/_components/home/Welcome.tsx
@@ -1,6 +1,8 @@
+import Player from "../common/Player";
+
export default function Welcome() {
return (
-
+
{"The Foundation for\nEvery Business Agent"}
@@ -12,18 +14,11 @@ export default function Welcome() {
-
diff --git a/app/_hooks/usePlayer.ts b/app/_hooks/usePlayer.ts
new file mode 100644
index 0000000..0b20c55
--- /dev/null
+++ b/app/_hooks/usePlayer.ts
@@ -0,0 +1,74 @@
+import { useRef, useState } from "react";
+import ReactPlayer, { ReactPlayerProps } from "react-player";
+
+export const usePlayer = ({ ...props }: ReactPlayerProps) => {
+ const { playing: initialPlaying, muted: initialMuted } = props;
+
+ const containerRef = useRef(null);
+ const playerRef = useRef(null);
+
+ const [playing, setPlaying] = useState(initialPlaying);
+ const [muted, setMuted] = useState(initialMuted);
+ const [played, setPlayed] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [seeking, setSeeking] = useState(false);
+ const [isFullScreen, setIsFullScreen] = useState(false);
+
+ const handlePlayPause = () => {
+ setPlaying((prev) => !prev);
+ };
+
+ const handleMuted = () => {
+ setMuted((prev) => !prev);
+ };
+
+ const handleSeekMouseDown = () => {
+ setSeeking(true);
+ };
+
+ const handleSeekChange = (e: React.ChangeEvent) => {
+ setPlayed(parseFloat(e.target.value));
+ };
+
+ const handleSeekMouseUp = (e: React.MouseEvent) => {
+ setSeeking(false);
+ playerRef.current?.seekTo(parseFloat(e.currentTarget.value));
+ };
+
+ const handleProgress = (state: { played: number }) => {
+ if (!seeking) {
+ setPlayed(state.played);
+ }
+ };
+
+ const handleDuration = (dur: number) => {
+ setDuration(dur);
+ };
+
+ const handleFullscreen = () => {
+ if (!isFullScreen && containerRef.current) {
+ containerRef.current.requestFullscreen();
+ } else {
+ document.exitFullscreen();
+ }
+ setIsFullScreen((prev) => !prev);
+ };
+
+ return {
+ containerRef,
+ playerRef,
+ playing,
+ muted,
+ played,
+ duration,
+ isFullScreen,
+ handlePlayPause,
+ handleMuted,
+ handleSeekChange,
+ handleSeekMouseDown,
+ handleSeekMouseUp,
+ handleProgress,
+ handleDuration,
+ handleFullscreen,
+ };
+};
diff --git a/app/_lib/second-to-minutes.ts b/app/_lib/second-to-minutes.ts
new file mode 100644
index 0000000..786c80f
--- /dev/null
+++ b/app/_lib/second-to-minutes.ts
@@ -0,0 +1,9 @@
+export function secondsToMinutes(input: number | string): string {
+ const seconds = Number(input);
+
+ if (isNaN(seconds) || seconds < 0) return "0:00";
+
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+}
diff --git a/content/meet-our-new-member.mdx b/content/meet-our-new-member.mdx
index 8cb9e1b..670446f 100644
--- a/content/meet-our-new-member.mdx
+++ b/content/meet-our-new-member.mdx
@@ -7,6 +7,8 @@ author: Owen
thumbnail: "/contents/meet-our-new-member/thumbnail.png"
---
+import Player from "@/app/_components/common/Player";
+
# Introducing New Team Member

@@ -33,9 +35,7 @@ But what if we could leverage AI technology to bring the conversational depth an
# Real Thoughts Emerge Through Conversations
-
+
The Wrtn Labs team leveraged [**Agentica**](https://wrtnlabs.io/agentica/) to develop and test an interactive **Interview AI** that dynamically generates follow-up questions based on user responses. The approach involves clearly defining interview goals, the type of information desired, and the insights valuable to the product team. If a user provides a vague or brief response, the AI agent is designed to ask follow-up questions to gather more detailed and relevant information.
@@ -47,9 +47,7 @@ Yet, not every user initially provided detailed feedback. Those without specific
# Can AI Really Ask the Right Questions to User?
-
+
To effectively capture interview objectives and desired information, while dynamically generating appropriate follow-up questions based on user responses, meticulous refinement of the prompts was necessary. We analyzed questions and response patterns from actual face-to-face interviews and reflected various examples of interview guides, primary questions, and potential follow-up questions in the prompts.
@@ -61,9 +59,7 @@ Of course, due to the nature of Large Language Models (LLMs), prompts are not re
# Organizing Insights Through AI Agents
-
+
However, unlike structured surveys, conversational data gathered from many users proved challenging to organize clearly. Since insights were recorded conversationally, it was essential to structure them into charts or allow queries to focus on specific data points. Here, Agentica’s core functionality—the Connector—played a pivotal role. Within Agentica, any callable function can be turned into a Connector accessible by the AI agents whenever required. For example, when an Insight Extraction agent received a command such as "Show me user churn complaints," it could invoke a Connector to access records from the Interview agent, retrieve relevant data, and summarize the results clearly in text form. Thus, agents with different purposes and tools could communicate seamlessly to fulfill user requests.
diff --git a/package.json b/package.json
index 0757db7..dc3b144 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"prism-react-renderer": "^2.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-player": "^2.16.0",
"react-toastify": "^11.0.5",
"react-use": "^17.6.0",
"swiper": "^11.2.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ba37bc0..4e94cb9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,6 +56,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
+ react-player:
+ specifier: ^2.16.0
+ version: 2.16.0(react@19.0.0)
react-toastify:
specifier: ^11.0.5
version: 11.0.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -1484,6 +1487,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -2339,6 +2346,9 @@ packages:
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
engines: {node: '>= 12.0.0'}
+ load-script@1.0.0:
+ resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
+
local-pkg@1.1.1:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'}
@@ -2449,6 +2459,9 @@ packages:
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
+ memoize-one@5.2.1:
+ resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -2933,6 +2946,9 @@ packages:
peerDependencies:
react: ^19.0.0
+ react-fast-compare@3.2.2:
+ resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -2942,6 +2958,11 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-player@2.16.0:
+ resolution: {integrity: sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==}
+ peerDependencies:
+ react: '>=16.6.0'
+
react-toastify@11.0.5:
resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==}
peerDependencies:
@@ -4985,6 +5006,8 @@ snapshots:
deep-is@0.1.4: {}
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -6083,6 +6106,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.29.2
lightningcss-win32-x64-msvc: 1.29.2
+ load-script@1.0.0: {}
+
local-pkg@1.1.1:
dependencies:
mlly: 1.7.4
@@ -6318,6 +6343,8 @@ snapshots:
mdn-data@2.0.14: {}
+ memoize-one@5.2.1: {}
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -7000,6 +7027,8 @@ snapshots:
react: 19.0.0
scheduler: 0.25.0
+ react-fast-compare@3.2.2: {}
+
react-is@16.13.1: {}
react-medium-image-zoom@5.2.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
@@ -7007,6 +7036,15 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
+ react-player@2.16.0(react@19.0.0):
+ dependencies:
+ deepmerge: 4.3.1
+ load-script: 1.0.0
+ memoize-one: 5.2.1
+ prop-types: 15.8.1
+ react: 19.0.0
+ react-fast-compare: 3.2.2
+
react-toastify@11.0.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
clsx: 2.1.1