Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
78 changes: 78 additions & 0 deletions app/_components/common/Player.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
ref={containerRef}
className={clsx(['relative w-full overflow-hidden rounded-4xl', aspect === "video" ? 'aspect-video' : 'aspect-square'])}
>
<ReactPlayer
ref={playerRef}
playing={playing}
muted={muted}
controls={false}
onProgress={handleProgress}
onDuration={handleDuration}
className="[&_video]:m-0"
width='100%'
height='100%'
playsinline
{...props}
/>

<div className="absolute bottom-0 left-0 flex flex-col gap-3 bg-linear-to-b px-6 pb-6 py-2 from-transparent to-[#6E6E68]/60 w-full">
<div className="flex gap-2 justify-between">
<div className="flex gap-2">
<button onClick={handlePlayPause} className={buttonStyle}>
{playing ? <Pause strokeWidth={1} size={16} /> : <Play strokeWidth={1} size={16} />}
</button>
<span className='h-11 w-[117px] flex items-center justify-center rounded-full bg-[#67675780] text-[#FFFFFF] text-lg'>
{secondsToMinutes((played * duration).toFixed(1))} / {secondsToMinutes(duration.toFixed(1))}
</span>

</div>

<div className="flex gap-2">
<button onClick={handleMuted} className={buttonStyle}>
{muted ? <VolumeX strokeWidth={1} size={16} /> : <Volume2 strokeWidth={1} size={16} />}
</button>

<button onClick={handleFullscreen} className={buttonStyle}>
{isFullScreen ? <Minimize2 strokeWidth={1} size={16} /> : <Maximize2 strokeWidth={1} size={16} />}
</button>
</div>
</div>

<input
type="range"
min={0}
max={1}
step="any"
value={played}
onMouseDown={handleSeekMouseDown}
onChange={handleSeekChange}
onMouseUp={handleSeekMouseUp}
className='appearance-none overflow-hidden cursor-pointer rounded-full [&::-webkit-slider-runnable-track]:bg-[#D4D4C9B2] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:shadow-[-1444px_0_0_1440px_#67675780] [&::-webkit-slider-thumb]:h-2 [&::-webkit-slider-thumb]:w-2 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#67675780]'
/>
</div>
</div>
)
}
21 changes: 8 additions & 13 deletions app/_components/home/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Player from "../common/Player";

export default function Welcome() {
return (
<section className="flex flex-col gap-24 pt-40 px-4 md:px-40">
<section className="flex w-full flex-col gap-24 pt-40 px-4 md:px-40">
<div className="flex flex-col items-center justify-center gap-9 text-center">
<h1 className="font-oceanic text-[32px] leading-[38px] md:whitespace-pre-line text-[#002424] md:text-[64px] md:leading-[69px]">
{"The Foundation for\nEvery Business Agent"}
Expand All @@ -12,18 +14,11 @@ export default function Welcome() {
</p>
</div>

<div className="max-x-[1440px] aspect-[1.7/1]">
<video
src="https://studio-pro-fe.s3.ap-northeast-2.amazonaws.com/preview.mp4"
autoPlay
loop
muted
controls
controlsList="nodownload"
disablePictureInPicture
playsInline
preload="auto"
className="w-full rounded-3xl"
<div className="max-x-[1440px]">
<Player
url="https://studio-pro-fe.s3.ap-northeast-2.amazonaws.com/preview.mp4"
playing={true}
muted={true}
/>
</div>
</section>
Expand Down
74 changes: 74 additions & 0 deletions app/_hooks/usePlayer.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const playerRef = useRef<ReactPlayer | null>(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<HTMLInputElement>) => {
setPlayed(parseFloat(e.target.value));
};

const handleSeekMouseUp = (e: React.MouseEvent<HTMLInputElement>) => {
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,
};
};
9 changes: 9 additions & 0 deletions app/_lib/second-to-minutes.ts
Original file line number Diff line number Diff line change
@@ -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")}`;
}
14 changes: 5 additions & 9 deletions content/meet-our-new-member.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

![Member](/contents/meet-our-new-member/1.png)
Expand All @@ -33,9 +35,7 @@ But what if we could leverage AI technology to bring the conversational depth an

# Real Thoughts Emerge Through Conversations

<video controls>
<source src="/contents/meet-our-new-member/3.mp4" type="video/mp4" />
</video>
<Player url="/contents/meet-our-new-member/3.mp4" aspect="square" />

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.

Expand All @@ -47,9 +47,7 @@ Yet, not every user initially provided detailed feedback. Those without specific

# Can AI Really Ask the Right Questions to User?

<video controls>
<source src="/contents/meet-our-new-member/4.mov" type="video/mp4" />
</video>
<Player url="/contents/meet-our-new-member/4.mov" aspect="square" />

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.

Expand All @@ -61,9 +59,7 @@ Of course, due to the nature of Large Language Models (LLMs), prompts are not re

# Organizing Insights Through AI Agents

<video controls>
<source src="/contents/meet-our-new-member/5.mp4" type="video/mp4" />
</video>
<Player url="/contents/meet-our-new-member/5.mp4" aspect="square" />

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.

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.