Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/firebaseai.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: firebaseai

on:
pull_request:
paths:
- 'firebaseai/**'
schedule:
# Run every day at 11pm (PST) - cron uses UTC times
- cron: '0 7 * * *'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

env:
SAMPLE: FirebaseAI

jobs:
spm:
name: spm (Xcode ${{ matrix.xcode }} - ${{ matrix.os }})
runs-on: macOS-15
strategy:
matrix:
xcode: ["16.3"]
os: [iOS]
include:
- os: iOS
device: iPhone 16
env:
SETUP: firebaseai
SPM: true
DIR: firebaseai
OS: ${{ matrix.os }}
DEVICE: ${{ matrix.device }}
TEST: false
XCODE_VERSION: ${{ matrix.xcode }}
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@master
- name: Setup
run: |
gem install xcpretty
- name: Placeholder GoogleService-Info.plist good enough for build only testing.
run: cp ./mock-GoogleService-Info.plist ./firebaseai/GoogleService-Info.plist
- name: Build and Test SwiftUI (${{ matrix.os }})
run: ./scripts/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions firebaseai/ChatSample/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
64 changes: 64 additions & 0 deletions firebaseai/ChatSample/Models/ChatMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2023 Google LLC
//
// 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
//
// http://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.

import Foundation

enum Participant {
case system
case user
}

struct ChatMessage: Identifiable, Equatable {
let id = UUID().uuidString
var message: String
let participant: Participant
var pending = false

static func pending(participant: Participant) -> ChatMessage {
Self(message: "", participant: participant, pending: true)
}
}

extension ChatMessage {
static var samples: [ChatMessage] = [
.init(message: "Hello. What can I do for you today?", participant: .system),
.init(message: "Show me a simple loop in Swift.", participant: .user),
.init(message: """
Sure, here is a simple loop in Swift:

# Example 1
```
for i in 1...5 {
print("Hello, world!")
}
```

This loop will print the string "Hello, world!" five times. The for loop iterates over a range of numbers,
in this case the numbers from 1 to 5. The variable i is assigned each number in the range, and the code inside the loop is executed.

**Here is another example of a simple loop in Swift:**
```swift
var sum = 0
for i in 1...100 {
sum += i
}
print("The sum of the numbers from 1 to 100 is \\(sum).")
```

This loop calculates the sum of the numbers from 1 to 100. The variable sum is initialized to 0, and then the for loop iterates over the range of numbers from 1 to 100. The variable i is assigned each number in the range, and the value of i is added to the sum variable. After the loop has finished executing, the value of sum is printed to the console.
""", participant: .system),
]

static var sample = samples[0]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
129 changes: 129 additions & 0 deletions firebaseai/ChatSample/Screens/ConversationScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2023 Google LLC
//
// 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
//
// http://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.

import FirebaseAI
import GenerativeAIUIComponents
import SwiftUI

struct ConversationScreen: View {
@EnvironmentObject
var viewModel: ConversationViewModel

@State
private var userPrompt = ""

enum FocusedField: Hashable {
case message
}

@FocusState
var focusedField: FocusedField?

var body: some View {
VStack {
ScrollViewReader { scrollViewProxy in
List {
ForEach(viewModel.messages) { message in
MessageView(message: message)
}
if let error = viewModel.error {
ErrorView(error: error)
.tag("errorView")
}
}
.listStyle(.plain)
.onChange(of: viewModel.messages, perform: { newValue in
if viewModel.hasError {
// wait for a short moment to make sure we can actually scroll to the bottom
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
withAnimation {
scrollViewProxy.scrollTo("errorView", anchor: .bottom)
}
focusedField = .message
}
} else {
guard let lastMessage = viewModel.messages.last else { return }

// wait for a short moment to make sure we can actually scroll to the bottom
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
withAnimation {
scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom)
}
focusedField = .message
}
}
})
}
InputField("Message...", text: $userPrompt) {
Image(systemName: viewModel.busy ? "stop.circle.fill" : "arrow.up.circle.fill")
.font(.title)
}
.focused($focusedField, equals: .message)
.onSubmit { sendOrStop() }
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: newChat) {
Image(systemName: "square.and.pencil")
}
}
}
.navigationTitle("Chat sample")
.onAppear {
focusedField = .message
}
}

private func sendMessage() {
Task {
let prompt = userPrompt
userPrompt = ""
await viewModel.sendMessage(prompt, streaming: true)
}
}

private func sendOrStop() {
focusedField = nil

if viewModel.busy {
viewModel.stop()
} else {
sendMessage()
}
}

private func newChat() {
viewModel.startNewChat()
}
}

struct ConversationScreen_Previews: PreviewProvider {
struct ContainerView: View {
@StateObject var viewModel = ConversationViewModel()

var body: some View {
ConversationScreen()
.environmentObject(viewModel)
.onAppear {
viewModel.messages = ChatMessage.samples
}
}
}

static var previews: some View {
NavigationStack {
ConversationScreen()
}
}
}
Loading