bleam 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/ai.cjs +2315 -2173
  2. package/dist/ai.d.cts +227 -322
  3. package/dist/ai.d.ts +227 -322
  4. package/dist/ai.js +2310 -2159
  5. package/dist/app-storage-D8W4n8ey.cjs +39 -0
  6. package/dist/app-storage-Isi5Bo0R.js +34 -0
  7. package/dist/cli.cjs +148 -22
  8. package/dist/cli.d.cts +14 -1
  9. package/dist/cli.d.ts +13 -0
  10. package/dist/cli.js +148 -22
  11. package/dist/config.d.cts +1 -1
  12. package/dist/elements-CFk0QHw0.d.cts +127 -0
  13. package/dist/{ui-CHc4xEs_.d.ts → elements-ClGQ41Sc.d.ts} +84 -49
  14. package/dist/{ui.cjs → elements.cjs} +307 -168
  15. package/dist/elements.d.cts +2 -0
  16. package/dist/elements.d.ts +2 -0
  17. package/dist/{ui.js → elements.js} +306 -169
  18. package/dist/{files-Ds1wT8C2.js → files-DErLhzCB.js} +11 -12
  19. package/dist/{files-Bo7h9fik.cjs → files-lMk-CpL_.cjs} +16 -11
  20. package/dist/files.cjs +1 -1
  21. package/dist/files.d.cts +1 -1
  22. package/dist/files.js +1 -1
  23. package/dist/index.d.cts +1 -1
  24. package/dist/schema-B7ELMpuI.js +226 -0
  25. package/dist/schema-B7SLUBLN.cjs +286 -0
  26. package/dist/schema-BWsDPc6c.d.cts +125 -0
  27. package/dist/schema-LxnzAfgw.d.ts +125 -0
  28. package/dist/schema.cjs +10 -2
  29. package/dist/schema.d.cts +2 -2
  30. package/dist/schema.d.ts +2 -2
  31. package/dist/schema.js +2 -2
  32. package/dist/secrets.cjs +146 -0
  33. package/dist/secrets.d.cts +14 -0
  34. package/dist/secrets.d.ts +14 -0
  35. package/dist/secrets.js +142 -0
  36. package/dist/state-BZYyrE2-.cjs +936 -0
  37. package/dist/state-DkaRFkZJ.js +843 -0
  38. package/dist/state.cjs +17 -12
  39. package/dist/state.d.cts +144 -140
  40. package/dist/state.d.ts +145 -140
  41. package/dist/state.js +4 -3
  42. package/dist/window.d.cts +1 -1
  43. package/dist/window.d.ts +3 -3
  44. package/package.json +13 -6
  45. package/templates/basic/app/index.tsx +2 -2
  46. package/templates/foundation-models/app/index.tsx +78 -16
  47. package/templates/image-generation/app/index.tsx +4 -4
  48. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +46 -46
  49. package/templates/native/ios/Podfile.lock +173 -173
  50. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +42 -365
  51. package/templates/state/app/index.tsx +2 -2
  52. package/templates/text-generation/app/index.tsx +83 -52
  53. package/templates/updates/README.md +1 -1
  54. package/dist/schema-Bo5Jvqus.js +0 -90
  55. package/dist/schema-CYh6n8GS.d.ts +0 -58
  56. package/dist/schema-oeOrd3i1.d.cts +0 -58
  57. package/dist/schema-rQ13mrpD.cjs +0 -102
  58. package/dist/state-Bx0VlTlO.cjs +0 -852
  59. package/dist/state-CAwe-Vw1.js +0 -767
  60. package/dist/ui-1WepaMS4.d.cts +0 -92
  61. package/dist/ui.d.cts +0 -2
  62. package/dist/ui.d.ts +0 -2
  63. /package/dist/{config-CufOVJV3.d.cts → config-COcRnn5a.d.cts} +0 -0
  64. /package/dist/{files-Dt5mbzLq.d.cts → files-DwA7pzr3.d.cts} +0 -0
  65. /package/dist/{native-sqlite-yQLD5s9i.cjs → native-sqlite-Dw--FI9a.cjs} +0 -0
  66. /package/dist/{native-sqlite-xcGdamRD.js → native-sqlite-WzRNzCSh.js} +0 -0
@@ -9,96 +9,10 @@ import FoundationModels
9
9
 
10
10
  public final class AIModule: Module {
11
11
  @MainActor private let textGeneration = TextGenerationCoordinator()
12
- private static var appGroupIdentifier: String {
13
- Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String ?? "group.dev.bleam.app"
14
- }
15
12
 
16
13
  public func definition() -> ModuleDefinition {
17
14
  Name("AI")
18
- Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate", "toolCall")
19
-
20
- Function("isAvailable") { () -> Bool in
21
- #if canImport(FoundationModels)
22
- if #available(iOS 26, *) {
23
- return SystemLanguageModel.default.availability == .available
24
- }
25
- #endif
26
- return false
27
- }
28
-
29
- Function("availability") { () -> [String: Any] in
30
- #if canImport(FoundationModels)
31
- if #available(iOS 26, *) {
32
- switch SystemLanguageModel.default.availability {
33
- case .available:
34
- return ["available": true]
35
- case .unavailable(.deviceNotEligible):
36
- return ["available": false, "reason": "device_not_eligible"]
37
- case .unavailable(.appleIntelligenceNotEnabled):
38
- return ["available": false, "reason": "apple_intelligence_not_enabled"]
39
- case .unavailable(.modelNotReady):
40
- return ["available": false, "reason": "model_not_ready"]
41
- case .unavailable:
42
- return ["available": false, "reason": "unknown"]
43
- }
44
- }
45
- #endif
46
- return ["available": false, "reason": "unknown"]
47
- }
48
-
49
- AsyncFunction("generate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
50
- #if canImport(FoundationModels)
51
- if #available(iOS 26, *) {
52
- guard SystemLanguageModel.default.availability == .available else {
53
- promise.reject("MODEL_UNAVAILABLE", "Apple Intelligence model is not available")
54
- return
55
- }
56
-
57
- guard !request.prompt.isEmpty else {
58
- promise.reject("INVALID_MESSAGE", "Expected prompt")
59
- return
60
- }
61
-
62
- let generationID = request.id ?? UUID().uuidString
63
- let task = Task { @MainActor [weak self] in
64
- guard let self else { return }
65
- do {
66
- let tools = try Self.tools(
67
- from: request.tools,
68
- generationID: generationID,
69
- coordinator: self.textGeneration,
70
- emit: { [weak self] event in self?.sendEvent("toolCall", event) }
71
- )
72
- let session = Self.session(system: request.system, tools: tools)
73
- let options = try Self.generationOptions(from: request)
74
-
75
- if let schema = request.outputSchema {
76
- let generationSchema = try Self.generationSchema(from: schema)
77
- let response = try await session.respond(
78
- to: request.prompt,
79
- schema: generationSchema,
80
- includeSchemaInPrompt: true,
81
- options: options
82
- )
83
- promise.resolve(try Self.value(from: response.content, schema: schema))
84
- return
85
- }
86
-
87
- let response = try await session.respond(to: request.prompt, options: options)
88
- promise.resolve(response.content)
89
- } catch {
90
- Self.reject(error, promise: promise)
91
- }
92
- self.textGeneration.finish(generationID)
93
- }
94
- Task { @MainActor [weak self] in self?.textGeneration.start(generationID, task: task) }
95
- } else {
96
- promise.reject("UNSUPPORTED_OS", "Apple Foundation Models require iOS 26 or newer")
97
- }
98
- #else
99
- promise.reject("UNSUPPORTED_OS", "Apple Foundation Models are unavailable in this build")
100
- #endif
101
- }
15
+ Events("imagegenProgress", "textGenerationUpdate", "localTextGenerationUpdate")
102
16
 
103
17
  AsyncFunction("streamGenerate") { [weak self] (request: TextGenerationRequest, promise: Promise) in
104
18
  #if canImport(FoundationModels)
@@ -116,13 +30,7 @@ public final class AIModule: Module {
116
30
  let task = Task { @MainActor [weak self] in
117
31
  guard let self else { return }
118
32
  do {
119
- let tools = try Self.tools(
120
- from: request.tools,
121
- generationID: generationID,
122
- coordinator: self.textGeneration,
123
- emit: { [weak self] event in self?.sendEvent("toolCall", event) }
124
- )
125
- let session = Self.session(system: request.system, tools: tools)
33
+ let session = try Self.session(system: request.system, messages: request.messages)
126
34
  let options = try Self.generationOptions(from: request)
127
35
  let stream = session.streamResponse(to: request.prompt, options: options)
128
36
  var content = ""
@@ -152,23 +60,6 @@ public final class AIModule: Module {
152
60
  return foundationCanceled || localCanceled
153
61
  }
154
62
 
155
- AsyncFunction("resolveToolCall") { [weak self] (request: ToolResultRequest) -> Bool in
156
- await self?.textGeneration.resolve(request) ?? false
157
- }
158
-
159
- AsyncFunction("generateLocalText") { (request: LocalTextGenerationRequest, promise: Promise) in
160
- Task { @MainActor in
161
- do {
162
- let event = try await ImageGenerationServiceClient.shared.runText(
163
- request: Self.bonsaiTextRequest(from: request)
164
- )
165
- promise.resolve(event.tokenIDs)
166
- } catch {
167
- Self.rejectLocalTextGeneration(error, promise: promise)
168
- }
169
- }
170
- }
171
-
172
63
  AsyncFunction("streamLocalText") { [weak self] (request: LocalTextGenerationRequest, promise: Promise) in
173
64
  Task { @MainActor in
174
65
  do {
@@ -488,13 +379,15 @@ public final class AIModule: Module {
488
379
 
489
380
  private static func workerRequest(from request: FluxImageRequest) throws -> GenerationWorkerRequest {
490
381
  let jobID = UUID()
491
- guard let appGroupContainer = FileManager.default.containerURL(
492
- forSecurityApplicationGroupIdentifier: appGroupIdentifier
493
- ) else {
494
- throw ModuleError.imageGeneration("Shared generation container is unavailable")
382
+ guard let cacheDirectory = FileManager.default.urls(
383
+ for: .cachesDirectory,
384
+ in: .userDomainMask
385
+ ).first else {
386
+ throw ModuleError.imageGeneration("Generation cache is unavailable")
495
387
  }
496
388
 
497
- let stagingDirectory = appGroupContainer
389
+ let stagingDirectory = cacheDirectory
390
+ .appendingPathComponent("Bleam", isDirectory: true)
498
391
  .appendingPathComponent("GenerationRuns", isDirectory: true)
499
392
  .appendingPathComponent("bleam-generation-jobs", isDirectory: true)
500
393
  .appendingPathComponent(jobID.uuidString, isDirectory: true)
@@ -627,31 +520,35 @@ public final class AIModule: Module {
627
520
 
628
521
  #if canImport(FoundationModels)
629
522
  @available(iOS 26, *)
630
- private static func session(system: String?, tools: [any Tool]) -> LanguageModelSession {
631
- if let system, !system.isEmpty {
632
- return LanguageModelSession(tools: tools, instructions: system)
523
+ private static func session(
524
+ system: String?,
525
+ messages: [TextMessageRequest]
526
+ ) throws -> LanguageModelSession {
527
+ if !messages.isEmpty {
528
+ var entries: [Transcript.Entry] = []
529
+ if system?.isEmpty == false {
530
+ entries.append(.instructions(Transcript.Instructions(
531
+ segments: system.map { [.text(Transcript.TextSegment(content: $0))] } ?? [],
532
+ toolDefinitions: []
533
+ )))
534
+ }
535
+ for message in messages {
536
+ let segments: [Transcript.Segment] = [.text(Transcript.TextSegment(content: message.content))]
537
+ switch message.role {
538
+ case "user":
539
+ entries.append(.prompt(Transcript.Prompt(segments: segments)))
540
+ case "assistant":
541
+ entries.append(.response(Transcript.Response(assetIDs: [], segments: segments)))
542
+ default:
543
+ throw ModuleError.invalidMessage("Unsupported transcript role: \(message.role)")
544
+ }
545
+ }
546
+ return LanguageModelSession(transcript: Transcript(entries: entries))
633
547
  }
634
- return LanguageModelSession(tools: tools)
635
- }
636
-
637
- @available(iOS 26, *)
638
- private static func tools(
639
- from definitions: [ToolDefinitionRequest],
640
- generationID: String,
641
- coordinator: TextGenerationCoordinator,
642
- emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
643
- ) throws -> [any Tool] {
644
- try definitions.map { definition in
645
- let parameters = try generationSchema(from: definition.inputSchema)
646
- return DynamicTool(
647
- name: definition.name,
648
- description: definition.description,
649
- parameters: parameters,
650
- generationID: generationID,
651
- coordinator: coordinator,
652
- emit: emit
653
- )
548
+ if let system, !system.isEmpty {
549
+ return LanguageModelSession(instructions: system)
654
550
  }
551
+ return LanguageModelSession()
655
552
  }
656
553
 
657
554
  @available(iOS 26, *)
@@ -663,131 +560,6 @@ public final class AIModule: Module {
663
560
  )
664
561
  }
665
562
 
666
- @available(iOS 26, *)
667
- private static func generationSchema(from schema: [String: Any]) throws -> GenerationSchema {
668
- do {
669
- return try GenerationSchema(root: dynamicSchema(from: schema), dependencies: [])
670
- } catch {
671
- throw ModuleError.invalidSchema(error.localizedDescription)
672
- }
673
- }
674
-
675
- @available(iOS 26, *)
676
- private static func dynamicSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
677
- guard let type = schema["type"] as? String else {
678
- throw ModuleError.invalidSchema("Missing schema type")
679
- }
680
-
681
- switch type {
682
- case "object":
683
- return try objectSchema(from: schema)
684
- case "array":
685
- guard let itemSchema = schema["items"] as? [String: Any] else {
686
- throw ModuleError.invalidSchema("Array schema must include items")
687
- }
688
- return try DynamicGenerationSchema(arrayOf: dynamicSchema(from: itemSchema))
689
- case "string":
690
- return DynamicGenerationSchema(type: String.self)
691
- case "number":
692
- return DynamicGenerationSchema(type: Double.self)
693
- case "boolean":
694
- return DynamicGenerationSchema(type: Bool.self)
695
- default:
696
- throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
697
- }
698
- }
699
-
700
- @available(iOS 26, *)
701
- private static func objectSchema(from schema: [String: Any]) throws -> DynamicGenerationSchema {
702
- let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
703
- let required = Set(schema["required"] as? [String] ?? [])
704
- var properties: [DynamicGenerationSchema.Property] = []
705
-
706
- for key in propertiesDictionary.keys.sorted() {
707
- guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
708
- throw ModuleError.invalidSchema("Property \(key) schema must be an object")
709
- }
710
-
711
- properties.append(
712
- DynamicGenerationSchema.Property(
713
- name: key,
714
- description: nil,
715
- schema: try dynamicSchema(from: propertySchema),
716
- isOptional: !required.contains(key)
717
- )
718
- )
719
- }
720
-
721
- return DynamicGenerationSchema(name: schema["title"] as? String ?? "Output", properties: properties)
722
- }
723
-
724
- @available(iOS 26, *)
725
- private static func value(from content: GeneratedContent, schema: [String: Any]) throws -> Any {
726
- guard let type = schema["type"] as? String else {
727
- throw ModuleError.invalidSchema("Missing schema type")
728
- }
729
-
730
- switch type {
731
- case "object":
732
- let propertiesDictionary = schema["properties"] as? [String: Any] ?? [:]
733
- let output = NSMutableDictionary()
734
-
735
- for key in propertiesDictionary.keys.sorted() {
736
- guard let propertySchema = propertiesDictionary[key] as? [String: Any] else {
737
- throw ModuleError.invalidSchema("Property \(key) schema must be an object")
738
- }
739
- output[key] = try propertyValue(from: content, property: key, schema: propertySchema)
740
- }
741
-
742
- return output
743
- default:
744
- throw ModuleError.invalidSchema("Root schema must be an object")
745
- }
746
- }
747
-
748
- @available(iOS 26, *)
749
- private static func propertyValue(
750
- from content: GeneratedContent,
751
- property: String,
752
- schema: [String: Any]
753
- ) throws -> Any {
754
- guard let type = schema["type"] as? String else {
755
- throw ModuleError.invalidSchema("Missing schema type")
756
- }
757
-
758
- switch type {
759
- case "string":
760
- return try content.value(String.self, forProperty: property)
761
- case "number":
762
- return try content.value(Double.self, forProperty: property)
763
- case "boolean":
764
- return try content.value(Bool.self, forProperty: property)
765
- case "object":
766
- let nestedContent = try content.value(GeneratedContent.self, forProperty: property)
767
- return try value(from: nestedContent, schema: schema)
768
- case "array":
769
- guard let itemSchema = schema["items"] as? [String: Any], let itemType = itemSchema["type"] as? String else {
770
- throw ModuleError.invalidSchema("Array schema must include items")
771
- }
772
-
773
- switch itemType {
774
- case "string":
775
- return try content.value([String].self, forProperty: property)
776
- case "number":
777
- return try content.value([Double].self, forProperty: property)
778
- case "boolean":
779
- return try content.value([Bool].self, forProperty: property)
780
- case "object":
781
- let values = try content.value([GeneratedContent].self, forProperty: property)
782
- return try values.map { try value(from: $0, schema: itemSchema) }
783
- default:
784
- throw ModuleError.invalidSchema("Unsupported array item type: \(itemType)")
785
- }
786
- default:
787
- throw ModuleError.invalidSchema("Unsupported schema type: \(type)")
788
- }
789
- }
790
-
791
563
  @available(iOS 26, *)
792
564
  private static func reject(_ error: Error, promise: Promise) {
793
565
  if error is CancellationError {
@@ -811,55 +583,14 @@ public final class AIModule: Module {
811
583
  }
812
584
  return
813
585
  }
814
- if let toolError = error as? LanguageModelSession.ToolCallError {
815
- promise.reject("TOOL_ERROR", toolError.localizedDescription)
816
- return
817
- }
818
-
819
586
  promise.reject(error)
820
587
  }
821
588
  #endif
822
589
  }
823
590
 
824
- #if canImport(FoundationModels)
825
- @available(iOS 26, *)
826
- private struct DynamicTool: Tool {
827
- typealias Arguments = GeneratedContent
828
-
829
- let name: String
830
- let description: String
831
- let parameters: GenerationSchema
832
- let generationID: String
833
- let coordinator: TextGenerationCoordinator
834
- let emit: @MainActor @Sendable ([String: Any]) -> Void
835
-
836
- func call(arguments: GeneratedContent) async throws -> String {
837
- let data = Data(arguments.jsonString.utf8)
838
- let value = try JSONSerialization.jsonObject(with: data)
839
- return try await coordinator.call(
840
- generationID: generationID,
841
- name: name,
842
- arguments: value,
843
- emit: emit
844
- )
845
- }
846
- }
847
- #endif
848
-
849
- private struct ToolExecutionError: LocalizedError {
850
- let message: String
851
- var errorDescription: String? { message }
852
- }
853
-
854
591
  @MainActor
855
592
  private final class TextGenerationCoordinator {
856
- private struct PendingToolCall {
857
- let generationID: String
858
- let continuation: CheckedContinuation<String, Error>
859
- }
860
-
861
593
  private var tasks: [String: Task<Void, Never>] = [:]
862
- private var toolCalls: [String: PendingToolCall] = [:]
863
594
 
864
595
  func start(_ id: String, task: Task<Void, Never>) {
865
596
  tasks[id]?.cancel()
@@ -868,67 +599,22 @@ private final class TextGenerationCoordinator {
868
599
 
869
600
  func finish(_ id: String) {
870
601
  tasks[id] = nil
871
- failToolCalls(for: id, error: CancellationError())
872
602
  }
873
603
 
874
604
  func cancel(_ id: String) -> Bool {
875
605
  guard let task = tasks.removeValue(forKey: id) else { return false }
876
606
  task.cancel()
877
- failToolCalls(for: id, error: CancellationError())
878
607
  return true
879
608
  }
880
-
881
- func call(
882
- generationID: String,
883
- name: String,
884
- arguments: Any,
885
- emit: @escaping @MainActor @Sendable ([String: Any]) -> Void
886
- ) async throws -> String {
887
- try Task.checkCancellation()
888
- let callID = UUID().uuidString
889
- return try await withCheckedThrowingContinuation { continuation in
890
- toolCalls[callID] = PendingToolCall(generationID: generationID, continuation: continuation)
891
- emit([
892
- "generationId": generationID,
893
- "callId": callID,
894
- "name": name,
895
- "arguments": arguments,
896
- ])
897
- }
898
- }
899
-
900
- func resolve(_ request: ToolResultRequest) -> Bool {
901
- guard let pending = toolCalls.removeValue(forKey: request.callId),
902
- pending.generationID == request.generationId
903
- else { return false }
904
-
905
- if let error = request.error {
906
- pending.continuation.resume(throwing: ToolExecutionError(message: error))
907
- } else {
908
- pending.continuation.resume(returning: request.output ?? "")
909
- }
910
- return true
911
- }
912
-
913
- private func failToolCalls(for generationID: String, error: Error) {
914
- let matches = toolCalls.filter { $0.value.generationID == generationID }
915
- for (id, pending) in matches {
916
- toolCalls[id] = nil
917
- pending.continuation.resume(throwing: error)
918
- }
919
- }
920
609
  }
921
610
 
922
611
  private enum ModuleError: Error, LocalizedError {
923
- case invalidSchema(String)
924
612
  case invalidMessage(String)
925
613
  case invalidModel(String)
926
614
  case imageGeneration(String)
927
615
 
928
616
  var errorDescription: String? {
929
617
  switch self {
930
- case .invalidSchema(let message):
931
- return "Invalid schema: \(message)"
932
618
  case .invalidMessage(let message), .invalidModel(let message), .imageGeneration(let message):
933
619
  return message
934
620
  }
@@ -1368,10 +1054,14 @@ private struct TextGenerationRequest: Record {
1368
1054
  @Field var id: String?
1369
1055
  @Field var system: String?
1370
1056
  @Field var prompt: String = ""
1371
- @Field var outputSchema: [String: Any]?
1057
+ @Field var messages: [TextMessageRequest] = []
1372
1058
  @Field var temperature: Double?
1373
1059
  @Field var maxTokens: Int?
1374
- @Field var tools: [ToolDefinitionRequest] = []
1060
+ }
1061
+
1062
+ private struct TextMessageRequest: Record {
1063
+ @Field var role: String = ""
1064
+ @Field var content: String = ""
1375
1065
  }
1376
1066
 
1377
1067
  private struct LocalTextGenerationRequest: Record {
@@ -1389,19 +1079,6 @@ private struct LocalTextGenerationRequest: Record {
1389
1079
  @Field var seed: Int?
1390
1080
  }
1391
1081
 
1392
- private struct ToolDefinitionRequest: Record {
1393
- @Field var name: String = ""
1394
- @Field var description: String = ""
1395
- @Field var inputSchema: [String: Any] = [:]
1396
- }
1397
-
1398
- private struct ToolResultRequest: Record {
1399
- @Field var generationId: String = ""
1400
- @Field var callId: String = ""
1401
- @Field var output: String?
1402
- @Field var error: String?
1403
- }
1404
-
1405
1082
  private struct ImageGenerationRequest: Record {
1406
1083
  @Field var model: String = ""
1407
1084
  @Field var prompt: String = ""
@@ -1,6 +1,6 @@
1
+ import { Button } from 'bleam/elements'
1
2
  import { atom, useAtom } from 'bleam/state'
2
3
  import { sx } from 'bleam/styles'
3
- import { Button } from 'bleam/ui'
4
4
  import { Window } from 'bleam/window'
5
5
  import { Text, View } from 'react-native'
6
6
 
@@ -20,7 +20,7 @@ export default function App() {
20
20
  </Text>
21
21
  ))}
22
22
  <Button
23
- title="Add task"
23
+ label="Add task"
24
24
  onPress={() =>
25
25
  setItems((current) => [...current, `Task ${current.length + 1}`])
26
26
  }
@@ -1,55 +1,48 @@
1
- import { bonsai, prepareTextModel, stream } from 'bleam/ai'
1
+ import { bonsai, chat, prepareTextModel, useChat, useChatList } from 'bleam/ai'
2
+ import { Button } from 'bleam/elements'
2
3
  import { sx } from 'bleam/styles'
3
- import { Button } from 'bleam/ui'
4
4
  import { Window } from 'bleam/window'
5
- import { useRef, useState } from 'react'
5
+ import { useEffect, useRef, useState } from 'react'
6
6
  import { Text, TextInput, View } from 'react-native'
7
7
 
8
8
  const appName = 'Bleam Basic'
9
9
  const model = bonsai('ternary-bonsai-4b')
10
10
 
11
11
  export default function App() {
12
- const [prompt, setPrompt] = useState(
13
- 'Rewrite this note so it is clear and concise: We should probably meet sometime next week to discuss the launch.',
14
- )
15
- const [answer, setAnswer] = useState('Runs privately on this Mac.')
16
- const [status, setStatus] = useState('Ready')
17
- const [isRunning, setIsRunning] = useState(false)
18
- const controller = useRef<AbortController>(null)
12
+ const chats = useChatList()
13
+ const creating = useRef(false)
14
+ const [path, setPath] = useState<string | null>(null)
15
+ const [status, setStatus] = useState('Preparing local model...')
19
16
 
20
- async function run() {
21
- if (controller.current) return
22
- const nextController = new AbortController()
23
- controller.current = nextController
24
- setIsRunning(true)
25
- setAnswer('')
17
+ useEffect(() => {
18
+ if (chats.length > 0 || creating.current) return
19
+ creating.current = true
20
+ void chat.create({ title: 'Private writing' }).catch((error) => {
21
+ setStatus(error instanceof Error ? error.message : String(error))
22
+ })
23
+ }, [chats.length])
26
24
 
27
- try {
28
- await prepareTextModel(model, {
29
- signal: nextController.signal,
30
- onProgress(progress) {
31
- setStatus(
32
- `Preparing model ${Math.round(progress.modelFraction * 100)}%`,
33
- )
34
- },
25
+ useEffect(() => {
26
+ const controller = new AbortController()
27
+ void prepareTextModel(model, {
28
+ signal: controller.signal,
29
+ onProgress(progress) {
30
+ setStatus(
31
+ `Preparing model ${Math.round(progress.modelFraction * 100)}%`,
32
+ )
33
+ },
34
+ })
35
+ .then((prepared) => {
36
+ setPath(prepared.path)
37
+ setStatus('Ready')
35
38
  })
36
- setStatus('Generating')
37
- for await (const text of stream({
38
- model,
39
- system: 'You are a concise private writing assistant.',
40
- prompt,
41
- signal: nextController.signal,
42
- })) {
43
- setAnswer(text)
44
- }
45
- setStatus('Complete')
46
- } catch (error) {
47
- setStatus(error instanceof Error ? error.message : String(error))
48
- } finally {
49
- if (controller.current === nextController) controller.current = null
50
- setIsRunning(false)
51
- }
52
- }
39
+ .catch((error) => {
40
+ if (!controller.signal.aborted) {
41
+ setStatus(error instanceof Error ? error.message : String(error))
42
+ }
43
+ })
44
+ return () => controller.abort()
45
+ }, [])
53
46
 
54
47
  return (
55
48
  <View style={sx('flex-1', 'p-8', 'gap-6')}>
@@ -58,14 +51,41 @@ export default function App() {
58
51
  Private Writing Assistant
59
52
  </Text>
60
53
  <Text style={sx('text-secondary-label')}>
61
- Ternary Bonsai runs locally after its first model download.
54
+ Ternary Bonsai runs locally after its first model download. Chats return
55
+ after relaunch.
62
56
  </Text>
57
+ <Text style={sx('text-secondary-label')}>{status}</Text>
58
+ {path && chats[0] ? <Conversation id={chats[0].id} path={path} /> : null}
59
+ </View>
60
+ )
61
+ }
62
+
63
+ function Conversation({ id, path }: { id: string; path: string }) {
64
+ const conversation = useChat(id, {
65
+ model,
66
+ path,
67
+ system: 'You are a concise private writing assistant.',
68
+ })
69
+ const [prompt, setPrompt] = useState(
70
+ 'Rewrite this note so it is clear and concise: We should probably meet sometime next week to discuss the launch.',
71
+ )
72
+ const latestAssistant = conversation.messages.findLast(
73
+ (message) => message.role === 'assistant',
74
+ )
75
+ const retryable =
76
+ latestAssistant?.status === 'failed' ||
77
+ latestAssistant?.status === 'canceled'
78
+ ? latestAssistant
79
+ : null
80
+
81
+ return (
82
+ <View style={sx('gap-6')}>
63
83
  <TextInput
64
84
  multiline
65
85
  value={prompt}
66
86
  onChangeText={setPrompt}
67
87
  style={sx(
68
- 'min-h-32',
88
+ { minHeight: 128 },
69
89
  'rounded-xl',
70
90
  'border',
71
91
  'border-separator',
@@ -74,20 +94,31 @@ export default function App() {
74
94
  />
75
95
  <View style={sx('flex-row', 'gap-3')}>
76
96
  <Button
77
- title="Generate"
78
- loading={isRunning}
79
- disabled={isRunning}
80
- onPress={() => void run()}
97
+ label="Send"
98
+ loading={conversation.isGenerating}
99
+ disabled={!conversation.canSend}
100
+ onPress={() => void conversation.send(prompt)}
81
101
  />
82
102
  <Button
83
- title="Cancel"
103
+ label="Cancel"
84
104
  variant="secondary"
85
- disabled={!isRunning}
86
- onPress={() => controller.current?.abort()}
105
+ disabled={!conversation.canCancel}
106
+ onPress={() => void conversation.cancel()}
87
107
  />
108
+ {retryable ? (
109
+ <Button
110
+ label="Retry"
111
+ variant="secondary"
112
+ onPress={() => void conversation.retry(retryable.id)}
113
+ />
114
+ ) : null}
88
115
  </View>
89
- <Text style={sx('text-secondary-label')}>{status}</Text>
90
- <Text style={sx('text-lg', 'text-label')}>{answer}</Text>
116
+ {conversation.messages.map((message) => (
117
+ <Text key={message.id} style={sx('text-lg', 'text-label')}>
118
+ {message.role === 'user' ? 'You: ' : 'Assistant: '}
119
+ {message.content || message.status}
120
+ </Text>
121
+ ))}
91
122
  </View>
92
123
  )
93
124
  }