examples/simple-chatbot: move clients to client directory
10
examples/simple-chatbot/client/android/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
24
examples/simple-chatbot/client/android/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, Daily
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
20
examples/simple-chatbot/client/android/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Pipecat Simple Chatbot Client for Android
|
||||
|
||||
Demo app which connects to the `simple-chatbot` backend over RTVI.
|
||||
|
||||
## Screenshot
|
||||
|
||||
<img alt="screenshot" src="files/screenshot.jpg" width="400px" />
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
./gradlew runDebug
|
||||
```
|
||||
|
||||
Ensure that the `simple-chatbot` server is running as described in the parent README.
|
||||
|
||||
For a full walkthrough, describing how to set up the backend and forward the connection
|
||||
to your Android device, please see the following blog post:
|
||||
|
||||
https://www.daily.co/blog/build-a-voice-agent-for-android-with-gemini-multimodal-live/
|
||||
5
examples/simple-chatbot/client/android/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
||||
plugins {
|
||||
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
}
|
||||
BIN
examples/simple-chatbot/client/android/files/screenshot.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
23
examples/simple-chatbot/client/android/gradle.properties
Normal file
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
@@ -0,0 +1,34 @@
|
||||
[versions]
|
||||
accompanistPermissions = "0.34.0"
|
||||
agp = "8.7.3"
|
||||
constraintlayoutCompose = "1.1.0"
|
||||
pipecatClientDaily = "0.3.2"
|
||||
kotlin = "2.0.20"
|
||||
coreKtx = "1.15.0"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.12.01"
|
||||
kotlinxSerializationJson = "1.7.1"
|
||||
kotlinxSerializationPlugin = "2.0.20"
|
||||
|
||||
[libraries]
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||
androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
pipecat-client-daily = { module = "ai.pipecat:daily-transport", version.ref = "pipecatClientDaily" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
|
||||
[plugins]
|
||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerializationPlugin" }
|
||||
BIN
examples/simple-chatbot/client/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
examples/simple-chatbot/client/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Mon Aug 05 13:01:27 BST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
examples/simple-chatbot/client/android/gradlew
vendored
Executable file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
examples/simple-chatbot/client/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
23
examples/simple-chatbot/client/android/settings.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Pipecat Simple Chatbot Client"
|
||||
include(":simple-chatbot-client")
|
||||
1
examples/simple-chatbot/client/android/simple-chatbot-client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,75 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.jetbrains.kotlin.serialization)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ai.pipecat.simple_chatbot_client"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ai.pipecat.simple_chatbot_client"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.pipecat.client.daily)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.androidx.constraintlayout.compose)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
}
|
||||
21
examples/simple-chatbot/client/android/simple-chatbot-client/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<application
|
||||
android:name=".RTVIApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.RTVIClient">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:theme="@style/Theme.RTVIClient">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,257 @@
|
||||
package ai.pipecat.simple_chatbot_client
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.ui.InCallLayout
|
||||
import ai.pipecat.simple_chatbot_client.ui.PermissionScreen
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.RTVIClientTheme
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.TextStyles
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.textFieldColors
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
val voiceClientManager = VoiceClientManager(this)
|
||||
|
||||
setContent {
|
||||
RTVIClientTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
PermissionScreen()
|
||||
|
||||
val vcState = voiceClientManager.state.value
|
||||
|
||||
if (vcState != null) {
|
||||
InCallLayout(voiceClientManager)
|
||||
|
||||
} else {
|
||||
ConnectSettings(voiceClientManager)
|
||||
}
|
||||
|
||||
voiceClientManager.errors.firstOrNull()?.let { errorText ->
|
||||
|
||||
val dismiss: () -> Unit = { voiceClientManager.errors.removeAt(0) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = dismiss,
|
||||
confirmButton = {
|
||||
Button(onClick = dismiss) {
|
||||
Text(
|
||||
text = "OK",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W700,
|
||||
color = Color.White,
|
||||
style = TextStyles.base
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = Color.White,
|
||||
title = {
|
||||
Text(
|
||||
text = "Error",
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color.Black,
|
||||
style = TextStyles.base
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = errorText.message,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W400,
|
||||
color = Color.Black,
|
||||
style = TextStyles.base
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ConnectSettings(
|
||||
voiceClientManager: VoiceClientManager,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val start = {
|
||||
val backendUrl = Preferences.backendUrl.value
|
||||
|
||||
voiceClientManager.start(baseUrl = backendUrl!!)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.imePadding()
|
||||
.padding(20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(2.dp, RoundedCornerShape(16.dp))
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Colors.mainSurfaceBackground)
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
vertical = 24.dp,
|
||||
horizontal = 28.dp
|
||||
)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = "Connect to an RTVI server",
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.W700,
|
||||
style = TextStyles.base
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
Text(
|
||||
text = "Backend URL",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W400,
|
||||
style = TextStyles.base
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(1.dp, Colors.textFieldBorder, RoundedCornerShape(12.dp)),
|
||||
value = Preferences.backendUrl.value ?: "",
|
||||
onValueChange = { Preferences.backendUrl.value = it },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Uri,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
colors = textFieldColors(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
ConnectDialogButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = start,
|
||||
text = "Connect",
|
||||
foreground = Color.White,
|
||||
background = Colors.buttonNormal,
|
||||
border = Colors.buttonNormal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectDialogButton(
|
||||
onClick: () -> Unit,
|
||||
text: String,
|
||||
foreground: Color,
|
||||
background: Color,
|
||||
border: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes icon: Int? = null,
|
||||
) {
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.border(1.dp, border, shape)
|
||||
.clip(shape)
|
||||
.background(background)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 10.dp, horizontal = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(icon),
|
||||
tint = foreground,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
color = foreground
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package ai.pipecat.simple_chatbot_client
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private val JSON_INSTANCE = Json { ignoreUnknownKeys = true }
|
||||
|
||||
object Preferences {
|
||||
|
||||
private const val PREF_BACKEND_URL = "backend_url"
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
fun initAppStart(context: Context) {
|
||||
prefs = context.applicationContext.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
|
||||
listOf(backendUrl).forEach { it.init() }
|
||||
}
|
||||
|
||||
private fun getString(key: String): String? = prefs.getString(key, null)
|
||||
|
||||
interface BasePref {
|
||||
fun init()
|
||||
}
|
||||
|
||||
class StringPref(private val key: String): BasePref {
|
||||
private val cachedValue = mutableStateOf<String?>(null)
|
||||
|
||||
override fun init() {
|
||||
cachedValue.value = getString(key)
|
||||
prefs.registerOnSharedPreferenceChangeListener { _, changedKey ->
|
||||
if (key == changedKey) {
|
||||
cachedValue.value = getString(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var value: String?
|
||||
get() = cachedValue.value
|
||||
set(newValue) {
|
||||
cachedValue.value = newValue
|
||||
prefs.edit().putString(key, newValue).apply()
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPref<E>(private val key: String, private var serializer: KSerializer<E>): BasePref {
|
||||
private val cachedValue = mutableStateOf<E?>(null)
|
||||
|
||||
private fun lookupValue(): E? =
|
||||
getString(key)?.let { JSON_INSTANCE.decodeFromString(serializer, it) }
|
||||
|
||||
override fun init() {
|
||||
cachedValue.value = lookupValue()
|
||||
prefs.registerOnSharedPreferenceChangeListener { _, changedKey ->
|
||||
if (key == changedKey) {
|
||||
cachedValue.value = lookupValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var value: E?
|
||||
get() = cachedValue.value
|
||||
set(newValue) {
|
||||
cachedValue.value = newValue
|
||||
prefs.edit()
|
||||
.putString(key, newValue?.let { JSON_INSTANCE.encodeToString(serializer, it) })
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
val backendUrl = StringPref(PREF_BACKEND_URL)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ai.pipecat.simple_chatbot_client
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class RTVIApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Preferences.initAppStart(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package ai.pipecat.simple_chatbot_client
|
||||
|
||||
import ai.pipecat.client.RTVIClient
|
||||
import ai.pipecat.client.RTVIClientOptions
|
||||
import ai.pipecat.client.RTVIClientParams
|
||||
import ai.pipecat.client.RTVIEventCallbacks
|
||||
import ai.pipecat.client.daily.DailyTransport
|
||||
import ai.pipecat.client.result.Future
|
||||
import ai.pipecat.client.result.RTVIError
|
||||
import ai.pipecat.client.result.Result
|
||||
import ai.pipecat.client.types.ActionDescription
|
||||
import ai.pipecat.client.types.Participant
|
||||
import ai.pipecat.client.types.PipecatMetrics
|
||||
import ai.pipecat.client.types.RTVIURLEndpoints
|
||||
import ai.pipecat.client.types.ServiceConfig
|
||||
import ai.pipecat.client.types.Tracks
|
||||
import ai.pipecat.client.types.Transcript
|
||||
import ai.pipecat.client.types.TransportState
|
||||
import ai.pipecat.simple_chatbot_client.utils.Timestamp
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
@Immutable
|
||||
data class Error(val message: String)
|
||||
|
||||
@Stable
|
||||
class VoiceClientManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VoiceClientManager"
|
||||
}
|
||||
|
||||
private val client = mutableStateOf<RTVIClient?>(null)
|
||||
|
||||
val state = mutableStateOf<TransportState?>(null)
|
||||
|
||||
val errors = mutableStateListOf<Error>()
|
||||
|
||||
val actionDescriptions =
|
||||
mutableStateOf<Result<List<ActionDescription>, RTVIError>?>(null)
|
||||
|
||||
val expiryTime = mutableStateOf<Timestamp?>(null)
|
||||
|
||||
val botReady = mutableStateOf(false)
|
||||
val botIsTalking = mutableStateOf(false)
|
||||
val userIsTalking = mutableStateOf(false)
|
||||
val botAudioLevel = mutableFloatStateOf(0f)
|
||||
val userAudioLevel = mutableFloatStateOf(0f)
|
||||
|
||||
val mic = mutableStateOf(false)
|
||||
val camera = mutableStateOf(false)
|
||||
val tracks = mutableStateOf<Tracks?>(null)
|
||||
|
||||
private fun <E> Future<E, RTVIError>.displayErrors() = withErrorCallback {
|
||||
Log.e(TAG, "Future resolved with error: ${it.description}", it.exception)
|
||||
errors.add(Error(it.description))
|
||||
}
|
||||
|
||||
fun start(baseUrl: String) {
|
||||
|
||||
if (client.value != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val options = RTVIClientOptions(
|
||||
params = RTVIClientParams(
|
||||
baseUrl = baseUrl,
|
||||
endpoints = RTVIURLEndpoints(),
|
||||
)
|
||||
)
|
||||
|
||||
state.value = TransportState.Disconnected
|
||||
|
||||
val callbacks = object : RTVIEventCallbacks() {
|
||||
override fun onTransportStateChanged(state: TransportState) {
|
||||
this@VoiceClientManager.state.value = state
|
||||
}
|
||||
|
||||
override fun onBackendError(message: String) {
|
||||
"Error from backend: $message".let {
|
||||
Log.e(TAG, it)
|
||||
errors.add(Error(it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBotReady(version: String, config: List<ServiceConfig>) {
|
||||
|
||||
Log.i(TAG, "Bot ready. Version $version, config: $config")
|
||||
|
||||
botReady.value = true
|
||||
|
||||
client.value?.describeActions()?.withCallback {
|
||||
actionDescriptions.value = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPipecatMetrics(data: PipecatMetrics) {
|
||||
Log.i(TAG, "Pipecat metrics: $data")
|
||||
}
|
||||
|
||||
override fun onUserTranscript(data: Transcript) {
|
||||
Log.i(TAG, "User transcript: $data")
|
||||
}
|
||||
|
||||
override fun onBotTranscript(text: String) {
|
||||
Log.i(TAG, "Bot transcript: $text")
|
||||
}
|
||||
|
||||
override fun onBotStartedSpeaking() {
|
||||
Log.i(TAG, "Bot started speaking")
|
||||
botIsTalking.value = true
|
||||
}
|
||||
|
||||
override fun onBotStoppedSpeaking() {
|
||||
Log.i(TAG, "Bot stopped speaking")
|
||||
botIsTalking.value = false
|
||||
}
|
||||
|
||||
override fun onUserStartedSpeaking() {
|
||||
Log.i(TAG, "User started speaking")
|
||||
userIsTalking.value = true
|
||||
}
|
||||
|
||||
override fun onUserStoppedSpeaking() {
|
||||
Log.i(TAG, "User stopped speaking")
|
||||
userIsTalking.value = false
|
||||
}
|
||||
|
||||
override fun onTracksUpdated(tracks: Tracks) {
|
||||
this@VoiceClientManager.tracks.value = tracks
|
||||
}
|
||||
|
||||
override fun onInputsUpdated(camera: Boolean, mic: Boolean) {
|
||||
this@VoiceClientManager.camera.value = camera
|
||||
this@VoiceClientManager.mic.value = mic
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
expiryTime.value = client.value?.expiry?.let(Timestamp::ofEpochSecs)
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
expiryTime.value = null
|
||||
actionDescriptions.value = null
|
||||
botIsTalking.value = false
|
||||
userIsTalking.value = false
|
||||
state.value = null
|
||||
actionDescriptions.value = null
|
||||
botReady.value = false
|
||||
tracks.value = null
|
||||
|
||||
client.value?.release()
|
||||
client.value = null
|
||||
}
|
||||
|
||||
override fun onUserAudioLevel(level: Float) {
|
||||
userAudioLevel.floatValue = level
|
||||
}
|
||||
|
||||
override fun onRemoteAudioLevel(level: Float, participant: Participant) {
|
||||
botAudioLevel.floatValue = level
|
||||
}
|
||||
}
|
||||
|
||||
val client = RTVIClient(DailyTransport.Factory(context), callbacks, options)
|
||||
|
||||
client.connect().displayErrors().withErrorCallback {
|
||||
callbacks.onDisconnected()
|
||||
}
|
||||
|
||||
this.client.value = client
|
||||
}
|
||||
|
||||
fun enableCamera(enabled: Boolean) {
|
||||
client.value?.enableCam(enabled)?.displayErrors()
|
||||
}
|
||||
|
||||
fun enableMic(enabled: Boolean) {
|
||||
client.value?.enableMic(enabled)?.displayErrors()
|
||||
}
|
||||
|
||||
fun toggleCamera() = enableCamera(!camera.value)
|
||||
fun toggleMic() = enableMic(!mic.value)
|
||||
|
||||
fun stop() {
|
||||
client.value?.disconnect()?.displayErrors()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
|
||||
@Composable
|
||||
fun ListeningAnimation(
|
||||
modifier: Modifier,
|
||||
active: Boolean,
|
||||
level: Float,
|
||||
color: Color,
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition("listeningAnimation")
|
||||
|
||||
val loopState by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = Math.PI.toFloat() * 2f,
|
||||
animationSpec = infiniteRepeatable(tween(durationMillis = 1000, easing = LinearEasing)),
|
||||
label = "listeningAnimationLoopState"
|
||||
)
|
||||
|
||||
val activeFraction by animateFloatAsState(
|
||||
if (active) {
|
||||
Math.pow(level.toDouble(), 0.3).toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
|
||||
Canvas(modifier.clearAndSetSemantics { }) {
|
||||
|
||||
val strokeWidthPx = size.width / 12
|
||||
|
||||
val lineCount = 5
|
||||
|
||||
for (i in 1..lineCount) {
|
||||
|
||||
val sine = Math.sin(loopState + 0.9 * i)
|
||||
val fraction = activeFraction * ((sine + 1) / 2).toFloat()
|
||||
|
||||
val x = (size.width / (lineCount + 1)) * i
|
||||
|
||||
val yMax = size.height * 0.25f
|
||||
val yMin = size.height * 0.5f
|
||||
|
||||
val y = yMin + (yMax - yMin) * fraction
|
||||
val yEnd = size.height - y
|
||||
|
||||
this.drawLine(
|
||||
start = Offset(x, y),
|
||||
end = Offset(x, yEnd),
|
||||
color = color,
|
||||
strokeWidth = strokeWidthPx,
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.FloatState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun BotIndicator(
|
||||
modifier: Modifier,
|
||||
isReady: Boolean,
|
||||
isTalking: State<Boolean>,
|
||||
audioLevel: FloatState,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(15.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val color by animateColorAsState(if (isTalking.value || !isReady) {
|
||||
Color.Black
|
||||
} else {
|
||||
Colors.botIndicatorBackground
|
||||
})
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.aspectRatio(1f)
|
||||
.fillMaxSize()
|
||||
.shadow(20.dp, CircleShape)
|
||||
.border(12.dp, Color.White, CircleShape)
|
||||
.border(1.dp, Colors.lightGrey, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.padding(50.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = isReady
|
||||
) { isReadyVal ->
|
||||
if (isReadyVal) {
|
||||
ListeningAnimation(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
active = isTalking.value,
|
||||
level = audioLevel.floatValue,
|
||||
color = Color.White
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(180.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 12.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
trackColor = color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewBotIndicator() {
|
||||
BotIndicator(
|
||||
modifier = Modifier,
|
||||
isReady = false,
|
||||
isTalking = remember { mutableStateOf(true) },
|
||||
audioLevel = remember { mutableFloatStateOf(1.0f) }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.R
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
private fun FooterButton(
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
@DrawableRes icon: Int,
|
||||
text: String,
|
||||
foreground: Color,
|
||||
background: Color,
|
||||
border: Color,
|
||||
) {
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.border(1.dp, border, shape)
|
||||
.clip(shape)
|
||||
.background(background)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 10.dp, horizontal = 18.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(icon),
|
||||
tint = foreground,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = foreground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.InCallFooter(
|
||||
onClickEnd: () -> Unit,
|
||||
) {
|
||||
Row(Modifier
|
||||
.fillMaxWidth(0.5f)
|
||||
.padding(15.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
FooterButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClickEnd,
|
||||
icon = R.drawable.phone_hangup,
|
||||
text = "End",
|
||||
foreground = Color.White,
|
||||
background = Colors.endButton,
|
||||
border = Colors.endButton
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.utils.Timestamp
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
|
||||
@Composable
|
||||
fun InCallHeader(
|
||||
expiryTime: Timestamp?
|
||||
) {
|
||||
ConstraintLayout(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp)
|
||||
) {
|
||||
val refTimer = createRef()
|
||||
|
||||
AnimatedContent(
|
||||
modifier = Modifier.constrainAs(refTimer) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
targetState = expiryTime,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||
) { expiryTimeVal ->
|
||||
if (expiryTimeVal != null) {
|
||||
Timer(expiryTime = expiryTimeVal, modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewInCallHeader() {
|
||||
InCallHeader(
|
||||
Timestamp.now() + java.time.Duration.ofMinutes(3)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.VoiceClientManager
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun InCallLayout(voiceClientManager: VoiceClientManager) {
|
||||
|
||||
val localCam by remember { derivedStateOf { voiceClientManager.tracks.value?.local?.video } }
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
|
||||
InCallHeader(expiryTime = voiceClientManager.expiryTime.value)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically)
|
||||
) {
|
||||
BotIndicator(
|
||||
modifier = Modifier,
|
||||
isReady = voiceClientManager.botReady.value,
|
||||
isTalking = voiceClientManager.botIsTalking,
|
||||
audioLevel = voiceClientManager.botAudioLevel
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
UserMicButton(
|
||||
onClick = voiceClientManager::toggleMic,
|
||||
micEnabled = voiceClientManager.mic.value,
|
||||
modifier = Modifier,
|
||||
isTalking = voiceClientManager.userIsTalking,
|
||||
audioLevel = voiceClientManager.userAudioLevel
|
||||
)
|
||||
|
||||
UserCamButton(
|
||||
onClick = voiceClientManager::toggleCamera,
|
||||
camEnabled = voiceClientManager.camera.value,
|
||||
camTrackId = localCam,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InCallFooter(
|
||||
onClickEnd = voiceClientManager::stop
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.TextStyles
|
||||
import android.Manifest
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun PermissionScreen() {
|
||||
val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)
|
||||
val micPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
|
||||
|
||||
val requestPermissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { isGranted ->
|
||||
Log.i("MainActivity", "Permissions granted: $isGranted")
|
||||
}
|
||||
|
||||
if (!cameraPermission.status.isGranted || !micPermission.status.isGranted) {
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
) {
|
||||
val dialogShape = RoundedCornerShape(16.dp)
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.shadow(6.dp, dialogShape)
|
||||
.border(2.dp, Colors.logoBorder, dialogShape)
|
||||
.clip(dialogShape)
|
||||
.background(Color.White)
|
||||
.padding(28.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Permissions",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.W700,
|
||||
style = TextStyles.base
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Please grant camera and mic permissions to continue",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.W400,
|
||||
style = TextStyles.base
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
Button(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
onClick = {
|
||||
requestPermissionLauncher.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Grant permissions",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W700,
|
||||
style = TextStyles.base
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.R
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import ai.pipecat.simple_chatbot_client.utils.Timestamp
|
||||
import ai.pipecat.simple_chatbot_client.utils.formatTimer
|
||||
import ai.pipecat.simple_chatbot_client.utils.rtcStateSecs
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import java.time.Duration
|
||||
|
||||
@Composable
|
||||
fun Timer(
|
||||
expiryTime: Timestamp,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val now by rtcStateSecs()
|
||||
|
||||
val shape = RoundedCornerShape(
|
||||
topStart = 12.dp,
|
||||
bottomStart = 12.dp,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.widthIn(min = 100.dp)
|
||||
.clip(shape)
|
||||
.background(Colors.lightGrey)
|
||||
.padding(top = 12.dp, bottom = 12.dp, start = 12.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.timer_outline),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Colors.expiryTimerForeground
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = formatTimer(duration = expiryTime - now),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Colors.expiryTimerForeground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewExpiryTimer() {
|
||||
Timer(Timestamp.now() + Duration.ofMinutes(5), Modifier)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.client.daily.VoiceClientVideoView
|
||||
import ai.pipecat.client.types.MediaTrackId
|
||||
import ai.pipecat.simple_chatbot_client.R
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
||||
@Composable
|
||||
fun UserCamButton(
|
||||
onClick: () -> Unit,
|
||||
camEnabled: Boolean,
|
||||
camTrackId: MediaTrackId?,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(15.dp).size(96.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val color by animateColorAsState(
|
||||
if (camEnabled) {
|
||||
Colors.unmutedMicBackground
|
||||
} else {
|
||||
Colors.mutedMicBackground
|
||||
}
|
||||
)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.shadow(3.dp, CircleShape)
|
||||
.border(6.dp, Color.White, CircleShape)
|
||||
.border(1.dp, Colors.lightGrey, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (camTrackId != null) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
VoiceClientVideoView(context)
|
||||
},
|
||||
update = { view ->
|
||||
view.voiceClientTrack = camTrackId
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier.size(30.dp),
|
||||
painter = painterResource(
|
||||
if (camEnabled) {
|
||||
R.drawable.video
|
||||
} else {
|
||||
R.drawable.video_off
|
||||
}
|
||||
),
|
||||
tint = Color.White,
|
||||
contentDescription = if (camEnabled) {
|
||||
"Disable camera"
|
||||
} else {
|
||||
"Enable camera"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewUserCamButton() {
|
||||
UserCamButton(
|
||||
onClick = {},
|
||||
camTrackId = null,
|
||||
camEnabled = true,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewUserCamButtonMuted() {
|
||||
UserCamButton(
|
||||
onClick = {},
|
||||
camTrackId = null,
|
||||
camEnabled = false,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.R
|
||||
import ai.pipecat.simple_chatbot_client.ui.theme.Colors
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.FloatState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun UserMicButton(
|
||||
onClick: () -> Unit,
|
||||
micEnabled: Boolean,
|
||||
modifier: Modifier,
|
||||
isTalking: State<Boolean>,
|
||||
audioLevel: FloatState,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(15.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val borderThickness by animateDpAsState(
|
||||
if (isTalking.value) {
|
||||
(24.dp * Math.pow(audioLevel.floatValue.toDouble(), 0.3).toFloat()) + 3.dp
|
||||
} else {
|
||||
6.dp
|
||||
}
|
||||
)
|
||||
|
||||
val color by animateColorAsState(
|
||||
if (!micEnabled) {
|
||||
Colors.mutedMicBackground
|
||||
} else if (isTalking.value) {
|
||||
Color.Black
|
||||
} else {
|
||||
Colors.unmutedMicBackground
|
||||
}
|
||||
)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.shadow(3.dp, CircleShape)
|
||||
.border(borderThickness, Color.White, CircleShape)
|
||||
.border(1.dp, Colors.lightGrey, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(36.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(48.dp),
|
||||
painter = painterResource(
|
||||
if (micEnabled) {
|
||||
R.drawable.microphone
|
||||
} else {
|
||||
R.drawable.microphone_off
|
||||
}
|
||||
),
|
||||
tint = Color.White,
|
||||
contentDescription = if (micEnabled) {
|
||||
"Mute microphone"
|
||||
} else {
|
||||
"Unmute microphone"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewUserMicButton() {
|
||||
UserMicButton(
|
||||
onClick = {},
|
||||
micEnabled = true,
|
||||
modifier = Modifier,
|
||||
isTalking = remember { mutableStateOf(false) },
|
||||
audioLevel = remember { mutableFloatStateOf(1.0f) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewUserMicButtonMuted() {
|
||||
UserMicButton(
|
||||
onClick = {},
|
||||
micEnabled = false,
|
||||
modifier = Modifier,
|
||||
isTalking = remember { mutableStateOf(false) },
|
||||
audioLevel = remember { mutableFloatStateOf(1.0f) }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
object Colors {
|
||||
val buttonNormal = Color(0xFF374151)
|
||||
val buttonWarning = Color(0xFFE53935)
|
||||
val buttonSection = Color(0xFFDFF1FF)
|
||||
|
||||
val activityBackground = Color(0xFFF9FAFB)
|
||||
val mainSurfaceBackground = Color.White
|
||||
|
||||
val lightGrey = Color(0x7FE5E7EB)
|
||||
val expiryTimerForeground = Color.Black
|
||||
val logoBorder = Color(0xFFE2E8F0)
|
||||
val endButton = Color(0xFF0F172A)
|
||||
val textFieldBorder = Color(0xFFDFE6EF)
|
||||
|
||||
val botIndicatorBackground = Color(0xFF374151)
|
||||
val mutedMicBackground = Color(0xFFF04A4A)
|
||||
val unmutedMicBackground = Color(0xFF616978)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Colors.buttonNormal,
|
||||
secondary = Colors.buttonWarning,
|
||||
background = Colors.activityBackground,
|
||||
surface = Colors.mainSurfaceBackground
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RTVIClientTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = LightColorScheme
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun textFieldColors() = TextFieldDefaults.colors().copy(
|
||||
unfocusedContainerColor = Colors.activityBackground,
|
||||
focusedContainerColor = Colors.activityBackground,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package ai.pipecat.simple_chatbot_client.ui.theme
|
||||
|
||||
import ai.pipecat.simple_chatbot_client.R
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
object TextStyles {
|
||||
val base = TextStyle(fontFamily = FontFamily(Font(R.font.inter)))
|
||||
}
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package ai.pipecat.simple_chatbot_client.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
private val rtcFlowSecs = flow {
|
||||
while(true) {
|
||||
val now = Timestamp.now().toEpochMilli()
|
||||
|
||||
val rounded = ((now + 500) / 1000) * 1000
|
||||
emit(Timestamp.ofEpochMilli(rounded))
|
||||
|
||||
val target = rounded + 1000
|
||||
delay(target - now)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rtcStateSecs() = rtcFlowSecs.collectAsState(initial = Timestamp.now())
|
||||
@@ -0,0 +1,64 @@
|
||||
package ai.pipecat.simple_chatbot_client.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Date
|
||||
|
||||
// Wrapper for Compose stability
|
||||
@Immutable
|
||||
@JvmInline
|
||||
value class Timestamp(
|
||||
val value: Instant
|
||||
) : Comparable<Timestamp> {
|
||||
val isInPast: Boolean
|
||||
get() = value < Instant.now()
|
||||
|
||||
val isInFuture: Boolean
|
||||
get() = value > Instant.now()
|
||||
|
||||
fun toEpochMilli() = value.toEpochMilli()
|
||||
|
||||
operator fun plus(duration: Duration) = Timestamp(value + duration)
|
||||
|
||||
operator fun minus(duration: Duration) = Timestamp(value - duration)
|
||||
|
||||
operator fun minus(rhs: Timestamp) = Duration.between(rhs.value, value)
|
||||
|
||||
override operator fun compareTo(other: Timestamp) = value.compareTo(other.value)
|
||||
|
||||
fun toISOString(): String = DateTimeFormatter.ISO_INSTANT.format(value)
|
||||
|
||||
override fun toString() = toISOString()
|
||||
|
||||
companion object {
|
||||
fun now() = Timestamp(Instant.now())
|
||||
|
||||
fun ofEpochMilli(value: Long) = Timestamp(Instant.ofEpochMilli(value))
|
||||
|
||||
fun ofEpochSecs(value: Long) = ofEpochMilli(value * 1000)
|
||||
|
||||
fun parse(value: CharSequence) = Timestamp(Instant.parse(value))
|
||||
|
||||
fun from(date: Date) = Timestamp(date.toInstant())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun formatTimer(duration: Duration): String {
|
||||
|
||||
if (duration.seconds < 0) {
|
||||
return "0s"
|
||||
}
|
||||
|
||||
val mins = duration.seconds / 60
|
||||
val secs = duration.seconds % 60
|
||||
|
||||
return if (mins == 0L) {
|
||||
"${secs}s"
|
||||
} else {
|
||||
"${mins}m ${secs}s"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/microphone.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/microphone_off.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/phone_hangup.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M12,9C10.4,9 8.85,9.25 7.4,9.72V12.82C7.4,13.22 7.17,13.56 6.84,13.72C5.86,14.21 4.97,14.84 4.17,15.57C4,15.75 3.75,15.86 3.5,15.86C3.2,15.86 2.95,15.74 2.77,15.56L0.29,13.08C0.11,12.9 0,12.65 0,12.38C0,12.1 0.11,11.85 0.29,11.67C3.34,8.77 7.46,7 12,7C16.54,7 20.66,8.77 23.71,11.67C23.89,11.85 24,12.1 24,12.38C24,12.65 23.89,12.9 23.71,13.08L21.23,15.56C21.05,15.74 20.8,15.86 20.5,15.86C20.25,15.86 20,15.75 19.82,15.57C19.03,14.84 18.14,14.21 17.16,13.72C16.83,13.56 16.6,13.22 16.6,12.82V9.72C15.15,9.25 13.6,9 12,9Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/timer_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M19.03,7.39L20.45,5.97C20,5.46 19.55,5 19.04,4.56L17.62,6C16.07,4.74 14.12,4 12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22C17,22 21,17.97 21,13C21,10.88 20.26,8.93 19.03,7.39M11,14H13V8H11M15,1H9V3H15V1Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/video.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" /></vector>
|
||||
@@ -0,0 +1 @@
|
||||
<!-- drawable/video_off.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M3.27,2L2,3.27L4.73,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16C16.2,18 16.39,17.92 16.54,17.82L19.73,21L21,19.73M21,6.5L17,10.5V7A1,1 0 0,0 16,6H9.82L21,17.18V6.5Z" /></vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Pipecat Simple Chatbot Client</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.RTVIClient" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
2
examples/simple-chatbot/client/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/SimpleChatbot.xcodeproj/xcuserdata/
|
||||
/SimpleChatbot.xcodeproj/project.xcworkspace/xcuserdata/
|
||||
18
examples/simple-chatbot/client/ios/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# iOS implementation
|
||||
|
||||
Basic implementation using the [Pipecat iOS SDK](https://docs.pipecat.ai/client/ios/introduction).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Run the bot server. See the [server README](../../server).
|
||||
2. Install [Xcode 15](https://developer.apple.com/xcode/), and set up your device [to run your own applications](https://developer.apple.com/documentation/xcode/distributing-your-app-to-registered-devices).
|
||||
|
||||
## Running locally
|
||||
|
||||
1. Clone this repository locally.
|
||||
2. Open the SimpleChatbot.xcodeproj in Xcode.
|
||||
3. Tell Xcode to update its Package Dependencies by clicking File -> Packages -> Update to Latest Package Versions.
|
||||
4. Build the project.
|
||||
5. Run the project on your device.
|
||||
6. Connect to the URL you are testing.
|
||||
|
||||
@@ -0,0 +1,704 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
90031FA72C616EE700408370 /* SimpleChatbotApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FA62C616EE700408370 /* SimpleChatbotApp.swift */; };
|
||||
90031FA92C616EE700408370 /* PreJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FA82C616EE700408370 /* PreJoinView.swift */; };
|
||||
90031FAB2C616EE800408370 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 90031FAA2C616EE800408370 /* Assets.xcassets */; };
|
||||
90031FAE2C616EE800408370 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 90031FAD2C616EE800408370 /* Preview Assets.xcassets */; };
|
||||
90031FB82C616EE900408370 /* SimpleChatbotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FB72C616EE900408370 /* SimpleChatbotTests.swift */; };
|
||||
90031FC22C616EE900408370 /* SimpleChatbotUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FC12C616EE900408370 /* SimpleChatbotUITests.swift */; };
|
||||
90031FC42C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FC32C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift */; };
|
||||
90031FDC2C6D5DD700408370 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90031FDB2C6D5DD700408370 /* ToastModifier.swift */; };
|
||||
90ABB98E2C735ED6000D9CC7 /* MeetingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB98D2C735ED6000D9CC7 /* MeetingView.swift */; };
|
||||
90ABB9902C736A8B000D9CC7 /* WaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB98F2C736A8B000D9CC7 /* WaveformView.swift */; };
|
||||
90ABB9932C73820D000D9CC7 /* MicrophoneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9922C73820D000D9CC7 /* MicrophoneView.swift */; };
|
||||
90ABB9982C738356000D9CC7 /* CustomColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9972C738356000D9CC7 /* CustomColors.swift */; };
|
||||
90ABB99A2C73A6A9000D9CC7 /* MockCallContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9992C73A6A9000D9CC7 /* MockCallContainerModel.swift */; };
|
||||
90ABB99D2C73C2D1000D9CC7 /* CallContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB99C2C73C2D1000D9CC7 /* CallContainerModel.swift */; };
|
||||
90ABB9A32C74E1CE000D9CC7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A22C74E1CE000D9CC7 /* SettingsView.swift */; };
|
||||
90ABB9A62C74EA8A000D9CC7 /* SettingsPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A52C74EA8A000D9CC7 /* SettingsPreference.swift */; };
|
||||
90ABB9A82C74EAB1000D9CC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90ABB9A72C74EAB1000D9CC7 /* SettingsManager.swift */; };
|
||||
90B6AB312D1359ED00DFF1D9 /* PipecatClientIOSDaily in Frameworks */ = {isa = PBXBuildFile; productRef = 90B6AB302D1359ED00DFF1D9 /* PipecatClientIOSDaily */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
90031FB42C616EE900408370 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 90031F9B2C616EE700408370 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 90031FA22C616EE700408370;
|
||||
remoteInfo = SimpleChatbot;
|
||||
};
|
||||
90031FBE2C616EE900408370 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 90031F9B2C616EE700408370 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 90031FA22C616EE700408370;
|
||||
remoteInfo = SimpleChatbot;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
90031FA32C616EE700408370 /* SimpleChatbot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleChatbot.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
90031FA62C616EE700408370 /* SimpleChatbotApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChatbotApp.swift; sourceTree = "<group>"; };
|
||||
90031FA82C616EE700408370 /* PreJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreJoinView.swift; sourceTree = "<group>"; };
|
||||
90031FAA2C616EE800408370 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
90031FAD2C616EE800408370 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
90031FB32C616EE900408370 /* SimpleChatbotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleChatbotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
90031FB72C616EE900408370 /* SimpleChatbotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChatbotTests.swift; sourceTree = "<group>"; };
|
||||
90031FBD2C616EE900408370 /* SimpleChatbotUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleChatbotUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
90031FC12C616EE900408370 /* SimpleChatbotUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChatbotUITests.swift; sourceTree = "<group>"; };
|
||||
90031FC32C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChatbotUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
90031FD62C63FD6A00408370 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
90031FDB2C6D5DD700408370 /* ToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModifier.swift; sourceTree = "<group>"; };
|
||||
90ABB98D2C735ED6000D9CC7 /* MeetingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingView.swift; sourceTree = "<group>"; };
|
||||
90ABB98F2C736A8B000D9CC7 /* WaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformView.swift; sourceTree = "<group>"; };
|
||||
90ABB9922C73820D000D9CC7 /* MicrophoneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrophoneView.swift; sourceTree = "<group>"; };
|
||||
90ABB9972C738356000D9CC7 /* CustomColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColors.swift; sourceTree = "<group>"; };
|
||||
90ABB9992C73A6A9000D9CC7 /* MockCallContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCallContainerModel.swift; sourceTree = "<group>"; };
|
||||
90ABB99C2C73C2D1000D9CC7 /* CallContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallContainerModel.swift; sourceTree = "<group>"; };
|
||||
90ABB9A22C74E1CE000D9CC7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
90ABB9A52C74EA8A000D9CC7 /* SettingsPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPreference.swift; sourceTree = "<group>"; };
|
||||
90ABB9A72C74EAB1000D9CC7 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
90031FA02C616EE700408370 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
90B6AB312D1359ED00DFF1D9 /* PipecatClientIOSDaily in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
90031FB02C616EE900408370 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
90031FBA2C616EE900408370 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
90031F9A2C616EE700408370 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90031FA52C616EE700408370 /* SimpleChatbot */,
|
||||
90031FB62C616EE900408370 /* SimpleChatbotTests */,
|
||||
90031FC02C616EE900408370 /* SimpleChatbotUITests */,
|
||||
90031FA42C616EE700408370 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90031FA42C616EE700408370 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90031FA32C616EE700408370 /* SimpleChatbot.app */,
|
||||
90031FB32C616EE900408370 /* SimpleChatbotTests.xctest */,
|
||||
90031FBD2C616EE900408370 /* SimpleChatbotUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90031FA52C616EE700408370 /* SimpleChatbot */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90ABB99B2C73C2C5000D9CC7 /* model */,
|
||||
90031FDD2C6D61E000408370 /* views */,
|
||||
90031FD62C63FD6A00408370 /* Info.plist */,
|
||||
90031FA62C616EE700408370 /* SimpleChatbotApp.swift */,
|
||||
90031FAA2C616EE800408370 /* Assets.xcassets */,
|
||||
90031FAC2C616EE800408370 /* Preview Content */,
|
||||
);
|
||||
path = SimpleChatbot;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90031FAC2C616EE800408370 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90031FAD2C616EE800408370 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90031FB62C616EE900408370 /* SimpleChatbotTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90031FB72C616EE900408370 /* SimpleChatbotTests.swift */,
|
||||
);
|
||||
path = SimpleChatbotTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90031FC02C616EE900408370 /* SimpleChatbotUITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90031FC12C616EE900408370 /* SimpleChatbotUITests.swift */,
|
||||
90031FC32C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift */,
|
||||
);
|
||||
path = SimpleChatbotUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90031FDD2C6D61E000408370 /* views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90ABB99E2C73C3A9000D9CC7 /* components */,
|
||||
90ABB9962C738346000D9CC7 /* extensions */,
|
||||
90ABB9A42C74EA52000D9CC7 /* settings */,
|
||||
90031FA82C616EE700408370 /* PreJoinView.swift */,
|
||||
90ABB98D2C735ED6000D9CC7 /* MeetingView.swift */,
|
||||
);
|
||||
path = views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90ABB9962C738346000D9CC7 /* extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90ABB9972C738356000D9CC7 /* CustomColors.swift */,
|
||||
);
|
||||
path = extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90ABB99B2C73C2C5000D9CC7 /* model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90ABB9992C73A6A9000D9CC7 /* MockCallContainerModel.swift */,
|
||||
90ABB99C2C73C2D1000D9CC7 /* CallContainerModel.swift */,
|
||||
);
|
||||
path = model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90ABB99E2C73C3A9000D9CC7 /* components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90ABB98F2C736A8B000D9CC7 /* WaveformView.swift */,
|
||||
90ABB9922C73820D000D9CC7 /* MicrophoneView.swift */,
|
||||
90031FDB2C6D5DD700408370 /* ToastModifier.swift */,
|
||||
);
|
||||
path = components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90ABB9A42C74EA52000D9CC7 /* settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90ABB9A52C74EA8A000D9CC7 /* SettingsPreference.swift */,
|
||||
90ABB9A72C74EAB1000D9CC7 /* SettingsManager.swift */,
|
||||
90ABB9A22C74E1CE000D9CC7 /* SettingsView.swift */,
|
||||
);
|
||||
path = settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
90031FA22C616EE700408370 /* SimpleChatbot */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 90031FC72C616EE900408370 /* Build configuration list for PBXNativeTarget "SimpleChatbot" */;
|
||||
buildPhases = (
|
||||
90031F9F2C616EE700408370 /* Sources */,
|
||||
90031FA02C616EE700408370 /* Frameworks */,
|
||||
90031FA12C616EE700408370 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SimpleChatbot;
|
||||
packageProductDependencies = (
|
||||
90B6AB302D1359ED00DFF1D9 /* PipecatClientIOSDaily */,
|
||||
);
|
||||
productName = SimpleChatbot;
|
||||
productReference = 90031FA32C616EE700408370 /* SimpleChatbot.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
90031FB22C616EE900408370 /* SimpleChatbotTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 90031FCA2C616EE900408370 /* Build configuration list for PBXNativeTarget "SimpleChatbotTests" */;
|
||||
buildPhases = (
|
||||
90031FAF2C616EE900408370 /* Sources */,
|
||||
90031FB02C616EE900408370 /* Frameworks */,
|
||||
90031FB12C616EE900408370 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
90031FB52C616EE900408370 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SimpleChatbotTests;
|
||||
productName = SimpleChatbotTests;
|
||||
productReference = 90031FB32C616EE900408370 /* SimpleChatbotTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
90031FBC2C616EE900408370 /* SimpleChatbotUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 90031FCD2C616EE900408370 /* Build configuration list for PBXNativeTarget "SimpleChatbotUITests" */;
|
||||
buildPhases = (
|
||||
90031FB92C616EE900408370 /* Sources */,
|
||||
90031FBA2C616EE900408370 /* Frameworks */,
|
||||
90031FBB2C616EE900408370 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
90031FBF2C616EE900408370 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SimpleChatbotUITests;
|
||||
productName = SimpleChatbotUITests;
|
||||
productReference = 90031FBD2C616EE900408370 /* SimpleChatbotUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
90031F9B2C616EE700408370 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
90031FA22C616EE700408370 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
90031FB22C616EE900408370 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
TestTargetID = 90031FA22C616EE700408370;
|
||||
};
|
||||
90031FBC2C616EE900408370 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
TestTargetID = 90031FA22C616EE700408370;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 90031F9E2C616EE700408370 /* Build configuration list for PBXProject "SimpleChatbot" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 90031F9A2C616EE700408370;
|
||||
packageReferences = (
|
||||
90B6AB2F2D1359ED00DFF1D9 /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */,
|
||||
);
|
||||
productRefGroup = 90031FA42C616EE700408370 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
90031FA22C616EE700408370 /* SimpleChatbot */,
|
||||
90031FB22C616EE900408370 /* SimpleChatbotTests */,
|
||||
90031FBC2C616EE900408370 /* SimpleChatbotUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
90031FA12C616EE700408370 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
90031FAE2C616EE800408370 /* Preview Assets.xcassets in Resources */,
|
||||
90031FAB2C616EE800408370 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
90031FB12C616EE900408370 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
90031FBB2C616EE900408370 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
90031F9F2C616EE700408370 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
90ABB99D2C73C2D1000D9CC7 /* CallContainerModel.swift in Sources */,
|
||||
90ABB9A62C74EA8A000D9CC7 /* SettingsPreference.swift in Sources */,
|
||||
90ABB99A2C73A6A9000D9CC7 /* MockCallContainerModel.swift in Sources */,
|
||||
90031FA92C616EE700408370 /* PreJoinView.swift in Sources */,
|
||||
90ABB9982C738356000D9CC7 /* CustomColors.swift in Sources */,
|
||||
90ABB98E2C735ED6000D9CC7 /* MeetingView.swift in Sources */,
|
||||
90ABB9A32C74E1CE000D9CC7 /* SettingsView.swift in Sources */,
|
||||
90ABB9932C73820D000D9CC7 /* MicrophoneView.swift in Sources */,
|
||||
90ABB9A82C74EAB1000D9CC7 /* SettingsManager.swift in Sources */,
|
||||
90031FDC2C6D5DD700408370 /* ToastModifier.swift in Sources */,
|
||||
90031FA72C616EE700408370 /* SimpleChatbotApp.swift in Sources */,
|
||||
90ABB9902C736A8B000D9CC7 /* WaveformView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
90031FAF2C616EE900408370 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
90031FB82C616EE900408370 /* SimpleChatbotTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
90031FB92C616EE900408370 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
90031FC42C616EE900408370 /* SimpleChatbotUITestsLaunchTests.swift in Sources */,
|
||||
90031FC22C616EE900408370 /* SimpleChatbotUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
90031FB52C616EE900408370 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 90031FA22C616EE700408370 /* SimpleChatbot */;
|
||||
targetProxy = 90031FB42C616EE900408370 /* PBXContainerItemProxy */;
|
||||
};
|
||||
90031FBF2C616EE900408370 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 90031FA22C616EE700408370 /* SimpleChatbot */;
|
||||
targetProxy = 90031FBE2C616EE900408370 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
90031FC52C616EE900408370 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
90031FC62C616EE900408370 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
90031FC82C616EE900408370 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"SimpleChatbot/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = EEBGKV9N3N;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SimpleChatbot/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = co.daily.SimpleChatbot;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
90031FC92C616EE900408370 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"SimpleChatbot/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = EEBGKV9N3N;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SimpleChatbot/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = co.daily.SimpleChatbot;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
90031FCB2C616EE900408370 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = EEBGKV9N3N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = co.daily.SimpleChatbotTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SimpleChatbot.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SimpleChatbot";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
90031FCC2C616EE900408370 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = EEBGKV9N3N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = co.daily.SimpleChatbotTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SimpleChatbot.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SimpleChatbot";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
90031FCE2C616EE900408370 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = EEBGKV9N3N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = co.daily.SimpleChatbotUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SimpleChatbot;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
90031FCF2C616EE900408370 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = EEBGKV9N3N;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = co.daily.SimpleChatbotUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SimpleChatbot;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
90031F9E2C616EE700408370 /* Build configuration list for PBXProject "SimpleChatbot" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
90031FC52C616EE900408370 /* Debug */,
|
||||
90031FC62C616EE900408370 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
90031FC72C616EE900408370 /* Build configuration list for PBXNativeTarget "SimpleChatbot" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
90031FC82C616EE900408370 /* Debug */,
|
||||
90031FC92C616EE900408370 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
90031FCA2C616EE900408370 /* Build configuration list for PBXNativeTarget "SimpleChatbotTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
90031FCB2C616EE900408370 /* Debug */,
|
||||
90031FCC2C616EE900408370 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
90031FCD2C616EE900408370 /* Build configuration list for PBXNativeTarget "SimpleChatbotUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
90031FCE2C616EE900408370 /* Debug */,
|
||||
90031FCF2C616EE900408370 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
90B6AB2F2D1359ED00DFF1D9 /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pipecat-ai/pipecat-client-ios-daily";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.3.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
90B6AB302D1359ED00DFF1D9 /* PipecatClientIOSDaily */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 90B6AB2F2D1359ED00DFF1D9 /* XCRemoteSwiftPackageReference "pipecat-client-ios-daily" */;
|
||||
productName = PipecatClientIOSDaily;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 90031F9B2C616EE700408370 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"originHash" : "72945fe9f3d71a3b546d5d2df153345b4dc9e72c00b2fda349508223e1a88501",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "daily-client-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daily-co/daily-client-ios.git",
|
||||
"state" : {
|
||||
"revision" : "15804ce495780da3ec2d05ab99736315f7bfbd24",
|
||||
"version" : "0.28.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "pipecat-client-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pipecat-ai/pipecat-client-ios.git",
|
||||
"state" : {
|
||||
"revision" : "d2d9946cf93f1e89290c724caa1631cffa439524",
|
||||
"version" : "0.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "pipecat-client-ios-daily",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pipecat-ai/pipecat-client-ios-daily",
|
||||
"state" : {
|
||||
"revision" : "cec4beef8762604c9a9b5ec760081ea2a0a697bf",
|
||||
"version" : "0.3.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "90031FA22C616EE700408370"
|
||||
BuildableName = "SimpleChatbot.app"
|
||||
BlueprintName = "SimpleChatbot"
|
||||
ReferencedContainer = "container:SimpleChatbot.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "90031FA22C616EE700408370"
|
||||
BuildableName = "SimpleChatbot.app"
|
||||
BlueprintName = "SimpleChatbot"
|
||||
ReferencedContainer = "container:SimpleChatbot.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "90031FA22C616EE700408370"
|
||||
BuildableName = "SimpleChatbot.app"
|
||||
BlueprintName = "SimpleChatbot"
|
||||
ReferencedContainer = "container:SimpleChatbot.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "appstore.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
examples/simple-chatbot/client/ios/SimpleChatbot/Assets.xcassets/pipecat.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Square Black.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="450" height="450" viewBox="0 0 450 450" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="450" height="450" fill="white"/>
|
||||
<path d="M104.772 129.77C109.448 128.01 114.725 129.331 118.02 133.086L160.936 182H289.064L331.98 133.086C335.275 129.331 340.552 128.01 345.228 129.77C349.904 131.531 353 136.004 353 141V249H391V273H329V172.873L303.52 201.915C301.242 204.511 297.955 206 294.5 206H155.5C152.045 206 148.758 204.511 146.48 201.915L121 172.873V273H59V249H97V141C97 136.004 100.096 131.531 104.772 129.77Z" fill="black"/>
|
||||
<path d="M329 297H391V321H329V297Z" fill="black"/>
|
||||
<path d="M59 297H121V321H59V297Z" fill="black"/>
|
||||
<path d="M187 257C187 265.837 179.837 273 171 273C162.163 273 155 265.837 155 257C155 248.164 162.163 241 171 241C179.837 241 187 248.164 187 257Z" fill="black"/>
|
||||
<path d="M295 257C295 265.837 287.837 273 279 273C270.163 273 263 265.837 263 257C263 248.164 270.163 241 279 241C287.837 241 295 248.164 295 257Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 982 B |
19
examples/simple-chatbot/client/ios/SimpleChatbot/Info.plist
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Camera is necessary for transmitting video in a call</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Microphone is necessary for transmitting audio in a call</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SimpleChatbotApp: App {
|
||||
|
||||
@StateObject var callContainerModel = CallContainerModel()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
if (!callContainerModel.isInCall) {
|
||||
PreJoinView().environmentObject(callContainerModel)
|
||||
} else {
|
||||
MeetingView().environmentObject(callContainerModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import SwiftUI
|
||||
|
||||
import RTVIClientIOSDaily
|
||||
import RTVIClientIOS
|
||||
|
||||
class CallContainerModel: ObservableObject {
|
||||
|
||||
@Published var voiceClientStatus: String = TransportState.disconnected.description
|
||||
@Published var isInCall: Bool = false
|
||||
@Published var isBotReady: Bool = false
|
||||
@Published var timerCount = 0
|
||||
|
||||
@Published var isMicEnabled: Bool = false
|
||||
|
||||
@Published var toastMessage: String? = nil
|
||||
@Published var showToast: Bool = false
|
||||
|
||||
@Published
|
||||
var remoteAudioLevel: Float = 0
|
||||
@Published
|
||||
var localAudioLevel: Float = 0
|
||||
|
||||
private var meetingTimer: Timer?
|
||||
|
||||
var rtviClientIOS: RTVIClient?
|
||||
|
||||
init() {
|
||||
// Changing the log level
|
||||
RTVIClientIOS.setLogLevel(.warn)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func connect(backendURL: String) {
|
||||
let baseUrl = backendURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if(baseUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty){
|
||||
self.showError(message: "Need to fill the backendURL. For more info visit: https://bots.daily.co")
|
||||
return
|
||||
}
|
||||
|
||||
let currentSettings = SettingsManager.getSettings()
|
||||
let rtviClientOptions = RTVIClientOptions.init(
|
||||
enableMic: currentSettings.enableMic,
|
||||
enableCam: false,
|
||||
params: RTVIClientParams(
|
||||
baseUrl: baseUrl,
|
||||
endpoints: RTVIURLEndpoints(connect: "/connect")
|
||||
)
|
||||
)
|
||||
self.rtviClientIOS = RTVIClient.init(
|
||||
transport: DailyTransport.init(options: rtviClientOptions),
|
||||
options: rtviClientOptions
|
||||
)
|
||||
self.rtviClientIOS?.delegate = self
|
||||
self.rtviClientIOS?.start() { result in
|
||||
if case .failure(let error) = result {
|
||||
self.showError(message: error.localizedDescription)
|
||||
self.rtviClientIOS = nil
|
||||
}
|
||||
}
|
||||
// Selecting the mic based on the preferences
|
||||
if let selectedMic = currentSettings.selectedMic {
|
||||
self.rtviClientIOS?.updateMic(micId: MediaDeviceId(id:selectedMic), completion: nil)
|
||||
}
|
||||
self.saveCredentials(backendURL: baseUrl)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func disconnect() {
|
||||
self.rtviClientIOS?.disconnect(completion: nil)
|
||||
}
|
||||
|
||||
func showError(message: String) {
|
||||
self.toastMessage = message
|
||||
self.showToast = true
|
||||
// Hide the toast after 5 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
self.showToast = false
|
||||
self.toastMessage = nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func toggleMicInput() {
|
||||
self.rtviClientIOS?.enableMic(enable: !self.isMicEnabled) { result in
|
||||
switch result {
|
||||
case .success():
|
||||
self.isMicEnabled = self.rtviClientIOS?.isMicEnabled ?? false
|
||||
case .failure(let error):
|
||||
self.showError(message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startTimer(withExpirationTime expirationTime: Int) {
|
||||
let currentTime = Int(Date().timeIntervalSince1970)
|
||||
self.timerCount = expirationTime - currentTime
|
||||
self.meetingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
|
||||
DispatchQueue.main.async {
|
||||
self.timerCount -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTimer() {
|
||||
self.meetingTimer?.invalidate()
|
||||
self.meetingTimer = nil
|
||||
self.timerCount = 0
|
||||
}
|
||||
|
||||
func saveCredentials(backendURL: String) {
|
||||
var currentSettings = SettingsManager.getSettings()
|
||||
currentSettings.backendURL = backendURL
|
||||
// Saving the settings
|
||||
SettingsManager.updateSettings(settings: currentSettings)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CallContainerModel:RTVIClientDelegate, LLMHelperDelegate {
|
||||
|
||||
private func handleEvent(eventName: String, eventValue: Any? = nil) {
|
||||
if let value = eventValue {
|
||||
print("RTVI Demo, received event:\(eventName), value:\(value)")
|
||||
} else {
|
||||
print("RTVI Demo, received event: \(eventName)")
|
||||
}
|
||||
}
|
||||
|
||||
func onTransportStateChanged(state: TransportState) {
|
||||
Task { @MainActor in
|
||||
self.handleEvent(eventName: "onTransportStateChanged", eventValue: state)
|
||||
self.voiceClientStatus = state.description
|
||||
self.isInCall = ( state == .connecting || state == .connected || state == .ready || state == .authenticating )
|
||||
}
|
||||
}
|
||||
|
||||
func onBotReady(botReadyData: BotReadyData) {
|
||||
Task { @MainActor in
|
||||
self.handleEvent(eventName: "onBotReady.")
|
||||
self.isBotReady = true
|
||||
if let expirationTime = self.rtviClientIOS?.expiry() {
|
||||
self.startTimer(withExpirationTime: expirationTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onConnected() {
|
||||
Task { @MainActor in
|
||||
self.isMicEnabled = self.rtviClientIOS?.isMicEnabled ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func onDisconnected() {
|
||||
Task { @MainActor in
|
||||
self.stopTimer()
|
||||
self.isBotReady = false
|
||||
}
|
||||
}
|
||||
|
||||
func onRemoteAudioLevel(level: Float, participant: Participant) {
|
||||
Task { @MainActor in
|
||||
self.remoteAudioLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
func onUserAudioLevel(level: Float) {
|
||||
Task { @MainActor in
|
||||
self.localAudioLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
func onUserTranscript(data: Transcript) {
|
||||
Task { @MainActor in
|
||||
if (data.final ?? false) {
|
||||
self.handleEvent(eventName: "onUserTranscript", eventValue: data.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onBotTranscript(data: String) {
|
||||
Task { @MainActor in
|
||||
self.handleEvent(eventName: "onBotTranscript", eventValue: data)
|
||||
}
|
||||
}
|
||||
|
||||
func onError(message: String) {
|
||||
Task { @MainActor in
|
||||
self.handleEvent(eventName: "onError", eventValue: message)
|
||||
self.showError(message: message)
|
||||
}
|
||||
}
|
||||
|
||||
func onTracksUpdated(tracks: Tracks) {
|
||||
Task { @MainActor in
|
||||
self.handleEvent(eventName: "onTracksUpdated", eventValue: tracks)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import SwiftUI
|
||||
import RTVIClientIOS
|
||||
|
||||
class MockCallContainerModel: CallContainerModel {
|
||||
|
||||
override init() {
|
||||
}
|
||||
|
||||
override func connect(backendURL: String) {
|
||||
print("connect")
|
||||
}
|
||||
|
||||
override func disconnect() {
|
||||
print("disconnect")
|
||||
}
|
||||
|
||||
override func showError(message: String) {
|
||||
self.toastMessage = message
|
||||
self.showToast = true
|
||||
// Hide the toast after 5 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
self.showToast = false
|
||||
self.toastMessage = nil
|
||||
}
|
||||
}
|
||||
|
||||
func startAudioLevelSimulation() {
|
||||
// Simulate audio level changes
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
||||
let newLevel = Float.random(in: 0...1)
|
||||
self.remoteAudioLevel = newLevel
|
||||
self.localAudioLevel = newLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MeetingView: View {
|
||||
|
||||
@State private var showingSettings = false
|
||||
@EnvironmentObject private var model: CallContainerModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Header Toolbar
|
||||
HStack {
|
||||
Image("dailyBot")
|
||||
.resizable()
|
||||
.frame(width: 48, height: 48)
|
||||
Spacer()
|
||||
HStack {
|
||||
Image(systemName: "stopwatch")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
Text(timerString(from: self.model.timerCount))
|
||||
.font(.headline)
|
||||
}.padding()
|
||||
.background(Color.timer)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.padding()
|
||||
|
||||
// Main Panel
|
||||
VStack {
|
||||
VStack {
|
||||
WaveformView(audioLevel: model.remoteAudioLevel, isBotReady: model.isBotReady, voiceClientStatus: model.voiceClientStatus)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
VStack {
|
||||
HStack {
|
||||
MicrophoneView(audioLevel: model.localAudioLevel, isMuted: !self.model.isMicEnabled)
|
||||
.frame(width: 160, height: 160)
|
||||
.onTapGesture {
|
||||
self.model.toggleMicInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 120)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
|
||||
// Bottom Panel
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.showingSettings = true
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "gearshape")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
Text("Settings")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.sheet(isPresented: $showingSettings) {
|
||||
SettingsView(showingSettings: $showingSettings).environmentObject(self.model)
|
||||
}
|
||||
}
|
||||
.border(Color.buttonsBorder, width: 1)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.foregroundColor(.black)
|
||||
.padding([.top, .horizontal])
|
||||
|
||||
Button(action: {
|
||||
self.model.disconnect()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
Text("End")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black)
|
||||
.cornerRadius(12)
|
||||
.padding([.bottom, .horizontal])
|
||||
}
|
||||
}
|
||||
.background(Color.backgroundApp)
|
||||
.toast(message: model.toastMessage, isShowing: model.showToast)
|
||||
}
|
||||
|
||||
func timerString(from count: Int) -> String {
|
||||
let hours = count / 3600
|
||||
let minutes = (count % 3600) / 60
|
||||
let seconds = count % 60
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let mockModel = MockCallContainerModel()
|
||||
let result = MeetingView().environmentObject(mockModel as CallContainerModel)
|
||||
mockModel.startAudioLevelSimulation()
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PreJoinView: View {
|
||||
|
||||
@State var backendURL: String
|
||||
|
||||
@EnvironmentObject private var model: CallContainerModel
|
||||
|
||||
init() {
|
||||
let currentSettings = SettingsManager.getSettings()
|
||||
self.backendURL = currentSettings.backendURL
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Image("pipecat")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
Text("Pipecat Client iOS.")
|
||||
.font(.headline)
|
||||
TextField("Server URL", text: $backendURL)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding([.bottom, .horizontal])
|
||||
Button("Connect") {
|
||||
self.model.connect(backendURL: self.backendURL)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity)
|
||||
.background(Color.backgroundApp)
|
||||
.toast(message: model.toastMessage, isShowing: model.showToast)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PreJoinView().environmentObject(MockCallContainerModel() as CallContainerModel)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MicrophoneView: View {
|
||||
var audioLevel: Float // Current audio level
|
||||
var isMuted: Bool // Muted state
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let width = geometry.size.width
|
||||
let circleSize = width * 0.9
|
||||
let innerCircleSize = width * 0.82
|
||||
let audioCircleSize = CGFloat(audioLevel) * (width * 0.95)
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
.frame(width: circleSize)
|
||||
|
||||
Circle()
|
||||
.fill(isMuted ? Color.disabledMic : Color.backgroundCircle)
|
||||
.frame(width: innerCircleSize)
|
||||
|
||||
if !isMuted {
|
||||
Circle()
|
||||
.fill(Color.micVolume)
|
||||
.opacity(0.5)
|
||||
.frame(width: audioCircleSize)
|
||||
.animation(.easeInOut(duration: 0.2), value: audioLevel)
|
||||
}
|
||||
|
||||
Image(systemName: isMuted ? "mic.slash.fill" : "mic.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: width * 0.2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity) // Ensures the ZStack is centered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MicrophoneView(audioLevel: 1, isMuted: false)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ToastModifier: ViewModifier {
|
||||
var message: String?
|
||||
var isShowing: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
ZStack {
|
||||
content
|
||||
if isShowing, let message = message {
|
||||
VStack {
|
||||
Text(message)
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.7))
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
.transition(.slide)
|
||||
.padding(.top, 50)
|
||||
Spacer()
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.5), value: isShowing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func toast(message: String?, isShowing: Bool) -> some View {
|
||||
self.modifier(ToastModifier(message: message, isShowing: isShowing))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WaveformView: View {
|
||||
|
||||
var audioLevel: Float
|
||||
var isBotReady: Bool
|
||||
var voiceClientStatus: String
|
||||
|
||||
@State
|
||||
private var audioLevels: [Float] = Array(repeating: 0, count: 5)
|
||||
private let dotCount = 5
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
ZStack {
|
||||
// Outer gray border
|
||||
Circle()
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
.frame(width: geometry.size.width * 0.9, height: geometry.size.width * 0.9)
|
||||
|
||||
// Gray middle
|
||||
Circle()
|
||||
.fill(isBotReady ? Color.backgroundCircle : Color.backgroundCircleNotConnected)
|
||||
.frame(width: geometry.size.width * 0.82, height: geometry.size.width * 0.82)
|
||||
|
||||
if isBotReady {
|
||||
if audioLevel > 0 {
|
||||
// Waveform bars inside the circle
|
||||
HStack(spacing: 10) {
|
||||
ForEach(0..<dotCount, id: \.self) { index in
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.frame(height: CGFloat(audioLevels[index]) * (geometry.size.height))
|
||||
.cornerRadius(12)
|
||||
.animation(.easeInOut(duration: 0.2), value: audioLevels[index])
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
.frame(width: geometry.size.width * 0.5, height: geometry.size.width * 0.5)
|
||||
.mask(Circle().frame(width: geometry.size.width * 0.82, height: geometry.size.width * 0.82))
|
||||
} else {
|
||||
// Dots inside the circle
|
||||
HStack(spacing: 10) {
|
||||
ForEach(0..<dotCount, id: \.self) { _ in
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.frame(width: geometry.size.width * 0.5, height: geometry.size.height * 0.5)
|
||||
}
|
||||
} else {
|
||||
// Gray circle with loading icon when not connected
|
||||
VStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(2) // Adjust size of the loading spinner
|
||||
.padding()
|
||||
Text(voiceClientStatus)
|
||||
.foregroundColor(.white)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onChange(of: audioLevel) { oldLevel, newLevel in
|
||||
// The audio level that we receive from the bot is usually too low
|
||||
// so just increasing it so we can see a better graph but
|
||||
// making sure that it is not higher than the maximum 1
|
||||
var audioLevel = audioLevel + 0.4
|
||||
if(audioLevel > 1) {
|
||||
audioLevel = 1
|
||||
}
|
||||
// Update the array and shift values
|
||||
audioLevels.removeFirst()
|
||||
audioLevels.append(newLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
WaveformView(audioLevel: 0, isBotReady: false, voiceClientStatus: "idle")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import SwiftUI
|
||||
|
||||
public extension Color {
|
||||
|
||||
static let backgroundCircle = Color(hex: "#374151")
|
||||
static let backgroundCircleNotConnected = Color(hex: "#D1D5DB")
|
||||
static let backgroundApp = Color(hex: "#F9FAFB")
|
||||
static let buttonsBorder = Color(hex: "#E5E7EB")
|
||||
static let micVolume = Color(hex: "#86EFAC")
|
||||
static let timer = Color(hex: "#E5E7EB")
|
||||
static let disabledMic = Color(hex: "#ee6b6e")
|
||||
static let disabledVision = Color(hex: "#BBF7D0")
|
||||
|
||||
init(hex: String) {
|
||||
let scanner = Scanner(string: hex)
|
||||
_ = scanner.scanString("#")
|
||||
|
||||
var rgb: UInt64 = 0
|
||||
scanner.scanHexInt64(&rgb)
|
||||
|
||||
let red = Double((rgb >> 16) & 0xFF) / 255.0
|
||||
let green = Double((rgb >> 8) & 0xFF) / 255.0
|
||||
let blue = Double(rgb & 0xFF) / 255.0
|
||||
|
||||
self.init(red: red, green: green, blue: blue)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
class SettingsManager {
|
||||
private static let preferencesKey = "settingsPreference"
|
||||
|
||||
static func getSettings() -> SettingsPreference {
|
||||
if let data = UserDefaults.standard.data(forKey: preferencesKey),
|
||||
let settings = try? JSONDecoder().decode(SettingsPreference.self, from: data) {
|
||||
return settings
|
||||
} else {
|
||||
// default values in case we don't have any settings
|
||||
return SettingsPreference(enableMic: true, backendURL: "http://YOUR_IP:7860")
|
||||
}
|
||||
}
|
||||
|
||||
static func updateSettings(settings: SettingsPreference) {
|
||||
if let data = try? JSONEncoder().encode(settings) {
|
||||
UserDefaults.standard.set(data, forKey: preferencesKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
struct SettingsPreference: Codable {
|
||||
var selectedMic: String?
|
||||
var enableMic: Bool
|
||||
var backendURL: String
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import SwiftUI
|
||||
import RTVIClientIOS
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@EnvironmentObject private var model: CallContainerModel
|
||||
|
||||
@Binding var showingSettings: Bool
|
||||
|
||||
@State private var selectedMic: MediaDeviceId? = nil
|
||||
@State private var isMicEnabled: Bool = true
|
||||
@State private var backendURL: String = ""
|
||||
|
||||
var body: some View {
|
||||
let microphones = self.model.rtviClientIOS?.getAllMics() ?? []
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("Audio Settings")) {
|
||||
List(microphones, id: \.self.id.id) { mic in
|
||||
Button(action: {
|
||||
self.selectMic(mic.id)
|
||||
}) {
|
||||
HStack {
|
||||
Text(mic.name)
|
||||
Spacer()
|
||||
if mic.id == self.selectedMic {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("Start options")) {
|
||||
Toggle("Enable Microphone", isOn: $isMicEnabled)
|
||||
}
|
||||
Section(header: Text("Server")) {
|
||||
TextField("Backend URL", text: $backendURL)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close") {
|
||||
self.saveSettings()
|
||||
self.showingSettings = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
self.loadSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectMic(_ mic: MediaDeviceId) {
|
||||
self.selectedMic = mic
|
||||
self.model.rtviClientIOS?.updateMic(micId: mic, completion: nil)
|
||||
}
|
||||
|
||||
private func saveSettings() {
|
||||
let newSettings = SettingsPreference(
|
||||
selectedMic: selectedMic?.id,
|
||||
enableMic: isMicEnabled,
|
||||
backendURL: backendURL
|
||||
)
|
||||
SettingsManager.updateSettings(settings: newSettings)
|
||||
}
|
||||
|
||||
private func loadSettings() {
|
||||
let savedSettings = SettingsManager.getSettings()
|
||||
if let selectedMic = savedSettings.selectedMic {
|
||||
self.selectedMic = MediaDeviceId(id: selectedMic)
|
||||
} else {
|
||||
self.selectedMic = nil
|
||||
}
|
||||
self.isMicEnabled = savedSettings.enableMic
|
||||
self.backendURL = savedSettings.backendURL
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let mockModel = MockCallContainerModel()
|
||||
let result = SettingsView(showingSettings: .constant(true)).environmentObject(mockModel as CallContainerModel)
|
||||
mockModel.startAudioLevelSimulation()
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import XCTest
|
||||
@testable import SimpleChatbot
|
||||
|
||||
final class SimpleChatbotTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
// Any test you write for XCTest can be annotated as throws and async.
|
||||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import XCTest
|
||||
|
||||
final class SimpleChatbotUITests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testLaunchPerformance() throws {
|
||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import XCTest
|
||||
|
||||
final class SimpleChatbotUITestsLaunchTests: XCTestCase {
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
func testLaunch() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
27
examples/simple-chatbot/client/javascript/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# JavaScript Implementation
|
||||
|
||||
Basic implementation using the [Pipecat JavaScript SDK](https://docs.pipecat.ai/client/js/introduction).
|
||||
|
||||
## Setup
|
||||
|
||||
1. Run the bot server. See the [server README](../../README).
|
||||
|
||||
2. Navigate to the `examples/javascript` directory:
|
||||
|
||||
```bash
|
||||
cd examples/javascript
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Run the client app:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
5. Visit http://localhost:5173 in your browser.
|
||||
40
examples/simple-chatbot/client/javascript/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Chatbot</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="status-bar">
|
||||
<div class="status">
|
||||
Status: <span id="connection-status">Disconnected</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="connect-btn">Connect</button>
|
||||
<button id="disconnect-btn" disabled>Disconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="bot-container">
|
||||
<div id="bot-video-container">
|
||||
</div>
|
||||
<audio id="bot-audio" autoplay></audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-panel">
|
||||
<h3>Debug Info</h3>
|
||||
<div id="debug-log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/app.js"></script>
|
||||
<link rel="stylesheet" href="/src/style.css">
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1249
examples/simple-chatbot/client/javascript/package-lock.json
generated
Normal file
21
examples/simple-chatbot/client/javascript/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@daily-co/realtime-ai-daily": "^0.2.1",
|
||||
"realtime-ai": "^0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.2"
|
||||
}
|
||||
}
|
||||
314
examples/simple-chatbot/client/javascript/src/app.js
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Copyright (c) 2025, Daily
|
||||
*
|
||||
* SPDX-License-Identifier: BSD 2-Clause License
|
||||
*/
|
||||
|
||||
/**
|
||||
* RTVI Client Implementation
|
||||
*
|
||||
* This client connects to an RTVI-compatible bot server using WebRTC (via Daily).
|
||||
* It handles audio/video streaming and manages the connection lifecycle.
|
||||
*
|
||||
* Requirements:
|
||||
* - A running RTVI bot server (defaults to http://localhost:7860)
|
||||
* - The server must implement the /connect endpoint that returns Daily.co room credentials
|
||||
* - Browser with WebRTC support
|
||||
*/
|
||||
|
||||
import { RTVIClient, RTVIEvent } from 'realtime-ai';
|
||||
import { DailyTransport } from '@daily-co/realtime-ai-daily';
|
||||
|
||||
/**
|
||||
* ChatbotClient handles the connection and media management for a real-time
|
||||
* voice and video interaction with an AI bot.
|
||||
*/
|
||||
class ChatbotClient {
|
||||
constructor() {
|
||||
// Initialize client state
|
||||
this.rtviClient = null;
|
||||
this.setupDOMElements();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up references to DOM elements and create necessary media elements
|
||||
*/
|
||||
setupDOMElements() {
|
||||
// Get references to UI control elements
|
||||
this.connectBtn = document.getElementById('connect-btn');
|
||||
this.disconnectBtn = document.getElementById('disconnect-btn');
|
||||
this.statusSpan = document.getElementById('connection-status');
|
||||
this.debugLog = document.getElementById('debug-log');
|
||||
this.botVideoContainer = document.getElementById('bot-video-container');
|
||||
|
||||
// Create an audio element for bot's voice output
|
||||
this.botAudio = document.createElement('audio');
|
||||
this.botAudio.autoplay = true;
|
||||
this.botAudio.playsInline = true;
|
||||
document.body.appendChild(this.botAudio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for connect/disconnect buttons
|
||||
*/
|
||||
setupEventListeners() {
|
||||
this.connectBtn.addEventListener('click', () => this.connect());
|
||||
this.disconnectBtn.addEventListener('click', () => this.disconnect());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a timestamped message to the debug log
|
||||
*/
|
||||
log(message) {
|
||||
const entry = document.createElement('div');
|
||||
entry.textContent = `${new Date().toISOString()} - ${message}`;
|
||||
|
||||
// Add styling based on message type
|
||||
if (message.startsWith('User: ')) {
|
||||
entry.style.color = '#2196F3'; // blue for user
|
||||
} else if (message.startsWith('Bot: ')) {
|
||||
entry.style.color = '#4CAF50'; // green for bot
|
||||
}
|
||||
|
||||
this.debugLog.appendChild(entry);
|
||||
this.debugLog.scrollTop = this.debugLog.scrollHeight;
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the connection status display
|
||||
*/
|
||||
updateStatus(status) {
|
||||
this.statusSpan.textContent = status;
|
||||
this.log(`Status: ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for available media tracks and set them up if present
|
||||
* This is called when the bot is ready or when the transport state changes to ready
|
||||
*/
|
||||
setupMediaTracks() {
|
||||
if (!this.rtviClient) return;
|
||||
|
||||
// Get current tracks from the client
|
||||
const tracks = this.rtviClient.tracks();
|
||||
|
||||
// Set up any available bot tracks
|
||||
if (tracks.bot?.audio) {
|
||||
this.setupAudioTrack(tracks.bot.audio);
|
||||
}
|
||||
if (tracks.bot?.video) {
|
||||
this.setupVideoTrack(tracks.bot.video);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up listeners for track events (start/stop)
|
||||
* This handles new tracks being added during the session
|
||||
*/
|
||||
setupTrackListeners() {
|
||||
if (!this.rtviClient) return;
|
||||
|
||||
// Listen for new tracks starting
|
||||
this.rtviClient.on(RTVIEvent.TrackStarted, (track, participant) => {
|
||||
// Only handle non-local (bot) tracks
|
||||
if (!participant?.local) {
|
||||
if (track.kind === 'audio') {
|
||||
this.setupAudioTrack(track);
|
||||
} else if (track.kind === 'video') {
|
||||
this.setupVideoTrack(track);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for tracks stopping
|
||||
this.rtviClient.on(RTVIEvent.TrackStopped, (track, participant) => {
|
||||
this.log(
|
||||
`Track stopped event: ${track.kind} from ${
|
||||
participant?.name || 'unknown'
|
||||
}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an audio track for playback
|
||||
* Handles both initial setup and track updates
|
||||
*/
|
||||
setupAudioTrack(track) {
|
||||
this.log('Setting up audio track');
|
||||
// Check if we're already playing this track
|
||||
if (this.botAudio.srcObject) {
|
||||
const oldTrack = this.botAudio.srcObject.getAudioTracks()[0];
|
||||
if (oldTrack?.id === track.id) return;
|
||||
}
|
||||
// Create a new MediaStream with the track and set it as the audio source
|
||||
this.botAudio.srcObject = new MediaStream([track]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a video track for display
|
||||
* Handles both initial setup and track updates
|
||||
*/
|
||||
setupVideoTrack(track) {
|
||||
this.log('Setting up video track');
|
||||
const videoEl = document.createElement('video');
|
||||
videoEl.autoplay = true;
|
||||
videoEl.playsInline = true;
|
||||
videoEl.muted = true;
|
||||
videoEl.style.width = '100%';
|
||||
videoEl.style.height = '100%';
|
||||
videoEl.style.objectFit = 'cover';
|
||||
|
||||
// Check if we're already displaying this track
|
||||
if (this.botVideoContainer.querySelector('video')?.srcObject) {
|
||||
const oldTrack = this.botVideoContainer
|
||||
.querySelector('video')
|
||||
.srcObject.getVideoTracks()[0];
|
||||
if (oldTrack?.id === track.id) return;
|
||||
}
|
||||
|
||||
// Create a new MediaStream with the track and set it as the video source
|
||||
videoEl.srcObject = new MediaStream([track]);
|
||||
this.botVideoContainer.innerHTML = '';
|
||||
this.botVideoContainer.appendChild(videoEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and connect to the bot
|
||||
* This sets up the RTVI client, initializes devices, and establishes the connection
|
||||
*/
|
||||
async connect() {
|
||||
try {
|
||||
// Create a new Daily transport for WebRTC communication
|
||||
const transport = new DailyTransport();
|
||||
|
||||
// Initialize the RTVI client with our configuration
|
||||
this.rtviClient = new RTVIClient({
|
||||
transport,
|
||||
params: {
|
||||
// The baseURL and endpoint of your bot server that the client will connect to
|
||||
baseUrl: 'http://localhost:7860',
|
||||
endpoints: {
|
||||
connect: '/connect',
|
||||
},
|
||||
},
|
||||
enableMic: true, // Enable microphone for user input
|
||||
enableCam: false,
|
||||
callbacks: {
|
||||
// Handle connection state changes
|
||||
onConnected: () => {
|
||||
this.updateStatus('Connected');
|
||||
this.connectBtn.disabled = true;
|
||||
this.disconnectBtn.disabled = false;
|
||||
this.log('Client connected');
|
||||
},
|
||||
onDisconnected: () => {
|
||||
this.updateStatus('Disconnected');
|
||||
this.connectBtn.disabled = false;
|
||||
this.disconnectBtn.disabled = true;
|
||||
this.log('Client disconnected');
|
||||
},
|
||||
// Handle transport state changes
|
||||
onTransportStateChanged: (state) => {
|
||||
this.updateStatus(`Transport: ${state}`);
|
||||
this.log(`Transport state changed: ${state}`);
|
||||
if (state === 'ready') {
|
||||
this.setupMediaTracks();
|
||||
}
|
||||
},
|
||||
// Handle bot connection events
|
||||
onBotConnected: (participant) => {
|
||||
this.log(`Bot connected: ${JSON.stringify(participant)}`);
|
||||
},
|
||||
onBotDisconnected: (participant) => {
|
||||
this.log(`Bot disconnected: ${JSON.stringify(participant)}`);
|
||||
},
|
||||
onBotReady: (data) => {
|
||||
this.log(`Bot ready: ${JSON.stringify(data)}`);
|
||||
this.setupMediaTracks();
|
||||
},
|
||||
// Transcript events
|
||||
onUserTranscript: (data) => {
|
||||
// Only log final transcripts
|
||||
if (data.final) {
|
||||
this.log(`User: ${data.text}`);
|
||||
}
|
||||
},
|
||||
onBotTranscript: (data) => {
|
||||
this.log(`Bot: ${data.text}`);
|
||||
},
|
||||
// Error handling
|
||||
onMessageError: (error) => {
|
||||
console.log('Message error:', error);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('Error:', error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Set up listeners for media track events
|
||||
this.setupTrackListeners();
|
||||
|
||||
// Initialize audio/video devices
|
||||
this.log('Initializing devices...');
|
||||
await this.rtviClient.initDevices();
|
||||
|
||||
// Connect to the bot
|
||||
this.log('Connecting to bot...');
|
||||
await this.rtviClient.connect();
|
||||
|
||||
this.log('Connection complete');
|
||||
} catch (error) {
|
||||
// Handle any errors during connection
|
||||
this.log(`Error connecting: ${error.message}`);
|
||||
this.log(`Error stack: ${error.stack}`);
|
||||
this.updateStatus('Error');
|
||||
|
||||
// Clean up if there's an error
|
||||
if (this.rtviClient) {
|
||||
try {
|
||||
await this.rtviClient.disconnect();
|
||||
} catch (disconnectError) {
|
||||
this.log(`Error during disconnect: ${disconnectError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the bot and clean up media resources
|
||||
*/
|
||||
async disconnect() {
|
||||
if (this.rtviClient) {
|
||||
try {
|
||||
// Disconnect the RTVI client
|
||||
await this.rtviClient.disconnect();
|
||||
this.rtviClient = null;
|
||||
|
||||
// Clean up audio
|
||||
if (this.botAudio.srcObject) {
|
||||
this.botAudio.srcObject.getTracks().forEach((track) => track.stop());
|
||||
this.botAudio.srcObject = null;
|
||||
}
|
||||
|
||||
// Clean up video
|
||||
if (this.botVideoContainer.querySelector('video')?.srcObject) {
|
||||
const video = this.botVideoContainer.querySelector('video');
|
||||
video.srcObject.getTracks().forEach((track) => track.stop());
|
||||
video.srcObject = null;
|
||||
}
|
||||
this.botVideoContainer.innerHTML = '';
|
||||
} catch (error) {
|
||||
this.log(`Error disconnecting: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the page loads
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new ChatbotClient();
|
||||
});
|
||||
98
examples/simple-chatbot/client/javascript/src/style.css
Normal file
@@ -0,0 +1,98 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 8px 16px;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#connect-btn {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#disconnect-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bot-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#bot-video-container {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin: 20px auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#bot-video-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.debug-panel h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#debug-log {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: #f8f8f8;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
15
examples/simple-chatbot/client/prebuilt/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Daily Prebuilt Connection
|
||||
|
||||
The simplest way to connect to the chatbot using Daily's Prebuilt UI.
|
||||
|
||||
1. Start the bot server
|
||||
|
||||
```bash
|
||||
python server/server.py
|
||||
```
|
||||
|
||||
2. Visit http://localhost:7860
|
||||
|
||||
3. Allow microphone access when prompted
|
||||
|
||||
4. Start talking with the bot
|
||||
24
examples/simple-chatbot/client/react/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
27
examples/simple-chatbot/client/react/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# React Implementation
|
||||
|
||||
Basic implementation using the [Pipecat React SDK](https://docs.pipecat.ai/client/react/introduction).
|
||||
|
||||
## Setup
|
||||
|
||||
1. Run the bot server; see [README](../../README).
|
||||
|
||||
2. Navigate to the `examples/react` directory:
|
||||
|
||||
```bash
|
||||
cd examples/react
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Run the client app:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
5. Visit http://localhost:5173 in your browser.
|
||||
28
examples/simple-chatbot/client/react/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
15
examples/simple-chatbot/client/react/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pipecat React Client</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3589
examples/simple-chatbot/client/react/package-lock.json
generated
Normal file
32
examples/simple-chatbot/client/react/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@daily-co/realtime-ai-daily": "^0.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"realtime-ai": "^0.2.1",
|
||||
"realtime-ai-react": "^0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.12.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^6.0.1"
|
||||
}
|
||||
}
|
||||