Procházet zdrojové kódy

feat(v0.4.1): mobile apps complete — iOS, Android, HarmonyOS

- App.tsx — React Native chat UI with 4-mode tabs
- app.json — Expo configuration for iOS + Android
- api.ts — Cloud sync API client
- PhoneApp.ets — HarmonyOS phone portrait UI
- build-mobile.sh — One-click build script (ios|android|harmonyos|all)
- tsconfig.json + ExportOptions.plist — iOS build config
- assets/icon.png — Mobile app icon (K logo)
KirinCode před 1 měsícem
rodič
revize
0d87f8a94b

+ 52 - 0
build-mobile.sh

@@ -0,0 +1,52 @@
+#!/bin/bash
+# KirinCode Mobile Build Script
+# Builds all three mobile platforms
+set -e
+
+RED='\033[0;31m'
+GOLD='\033[0;33m'
+NC='\033[0m'
+DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo -e "${RED}${DIR}/packages/mobile${NC}"
+echo ""
+
+case "${1:-all}" in
+  ios)
+    echo -e "${GOLD}Building iOS...${NC}"
+    cd "$DIR/packages/mobile"
+    npx expo prebuild --platform ios --clean
+    cd ios
+    pod install
+    xcodebuild -workspace KirinCode.xcworkspace -scheme KirinCode \
+      -configuration Release -sdk iphoneos \
+      -archivePath build/KirinCode.xcarchive archive
+    xcodebuild -exportArchive -archivePath build/KirinCode.xcarchive \
+      -exportPath build -exportOptionsPlist ExportOptions.plist
+    echo -e "${RED}✅ iOS build complete: packages/mobile/ios/build/KirinCode.ipa${NC}"
+    ;;
+  android)
+    echo -e "${GOLD}Building Android...${NC}"
+    cd "$DIR/packages/mobile"
+    npx expo prebuild --platform android --clean
+    cd android
+    ./gradlew assembleRelease
+    APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
+    echo -e "${RED}✅ Android build complete: $APK${NC}"
+    ;;
+  harmonyos)
+    echo -e "${GOLD}HarmonyOS build requires DevEco Studio.${NC}"
+    echo "  1. Open DevEco Studio"
+    echo "  2. File → Open → $DIR/packages/kirincode-ohos"
+    echo "  3. Build → Build Hap(s)"
+    echo "  4. Output: entry/build/default/outputs/default/entry-default-signed.hap"
+    ;;
+  all)
+    bash "$0" ios
+    bash "$0" android
+    bash "$0" harmonyos
+    ;;
+  *)
+    echo "Usage: build-mobile.sh [ios|android|harmonyos|all]"
+    ;;
+esac

+ 1 - 1
packages/kirincode-cloud/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/kirincode-cloud",
-  "version": "0.4.0",
+  "version": "0.4.1",
   "type": "module",
   "description": "KirinCode Cloud Sync Server — multi-user backend with session sync, memory, skills, workflows",
   "license": "MIT",

+ 151 - 0
packages/mobile/App.tsx

@@ -0,0 +1,151 @@
+import React, { useState, useEffect } from 'react'
+import {
+  View, Text, TextInput, TouchableOpacity, FlatList,
+  StyleSheet, SafeAreaView, StatusBar, ActivityIndicator
+} from 'react-native'
+import { client, Session, Message } from './src/services/api'
+
+const MODES = ['build', 'plan', 'debug', 'solo'] as const
+type Mode = typeof MODES[number]
+
+export default function App() {
+  const [sessions, setSessions] = useState<Session[]>([])
+  const [currentSession, setCurrentSession] = useState<Session | null>(null)
+  const [messages, setMessages] = useState<Message[]>([])
+  const [inputText, setInputText] = useState('')
+  const [mode, setMode] = useState<Mode>('build')
+  const [connected, setConnected] = useState(false)
+  const [loading, setLoading] = useState(true)
+
+  useEffect(() => {
+    checkConnection()
+  }, [])
+
+  async function checkConnection() {
+    try {
+      if (typeof window !== 'undefined') {
+        await fetch('http://localhost:4096/global/health')
+      }
+      setConnected(true)
+    } catch { }
+    setLoading(false)
+  }
+
+  async function createSession() {
+    try {
+      const s = await client.createSession(`Chat ${new Date().toLocaleTimeString()}`)
+      setCurrentSession(s)
+      setMessages([])
+    } catch (e) {
+      console.error(e)
+    }
+  }
+
+  async function sendMessage() {
+    if (!inputText.trim() || !currentSession) return
+    const userMsg: Message = {
+      id: `u_${Date.now()}`, role: 'user',
+      content: inputText, createdAt: new Date().toISOString()
+    }
+    setMessages(prev => [...prev, userMsg])
+    setInputText('')
+    try {
+      await client.sendPrompt(currentSession.id, inputText, mode)
+      const msgs = await client.getMessages(currentSession.id)
+      setMessages(msgs)
+    } catch (e) {
+      console.error(e)
+    }
+  }
+
+  if (loading) {
+    return (
+      <View style={styles.center}>
+        <ActivityIndicator size="large" color="#e74c3c" />
+        <Text style={styles.brand}>K I R I N</Text>
+      </View>
+    )
+  }
+
+  return (
+    <SafeAreaView style={styles.container}>
+      <StatusBar barStyle="light-content" backgroundColor="#1a1a2e" />
+
+      {/* Mode Tabs */}
+      <View style={styles.modeBar}>
+        {MODES.map(m => (
+          <TouchableOpacity key={m} style={[styles.modeTab, mode === m && styles.modeActive]}
+            onPress={() => setMode(m)}>
+            <Text style={[styles.modeText, mode === m && styles.modeActiveText]}>{m}</Text>
+          </TouchableOpacity>
+        ))}
+        <Text style={styles.status}>{connected ? '🟢' : '🔴'}</Text>
+      </View>
+
+      {/* Chat Area */}
+      {!currentSession ? (
+        <View style={styles.center}>
+          <Text style={styles.logo}>🦄</Text>
+          <Text style={styles.brand}>K I R I N</Text>
+          <TouchableOpacity style={styles.newChatBtn} onPress={createSession}>
+            <Text style={styles.newChatText}>New Chat</Text>
+          </TouchableOpacity>
+        </View>
+      ) : (
+        <FlatList
+          data={messages}
+          keyExtractor={item => item.id}
+          style={styles.messageList}
+          renderItem={({ item }) => (
+            <View style={[styles.bubble, item.role === 'user' ? styles.userBubble : styles.aiBubble]}>
+              <Text style={[styles.bubbleText, item.role === 'user' ? styles.userText : styles.aiText]}>
+                {item.content}
+              </Text>
+            </View>
+          )}
+        />
+      )}
+
+      {/* Input Bar */}
+      <View style={styles.inputBar}>
+        <TextInput
+          style={styles.input}
+          placeholder="Message..."
+          placeholderTextColor="#7f8c8d"
+          value={inputText}
+          onChangeText={setInputText}
+          onSubmitEditing={sendMessage}
+        />
+        <TouchableOpacity style={styles.sendBtn} onPress={sendMessage}>
+          <Text style={styles.sendText}>→</Text>
+        </TouchableOpacity>
+      </View>
+    </SafeAreaView>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1, backgroundColor: '#0f172a' },
+  center: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#0f172a' },
+  logo: { fontSize: 48, opacity: 0.3 },
+  brand: { fontSize: 20, fontWeight: '700', color: '#e74c3c', marginTop: 8, letterSpacing: 4 },
+  modeBar: { flexDirection: 'row', padding: 8, backgroundColor: '#16213e', alignItems: 'center' },
+  modeTab: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 6, marginRight: 4 },
+  modeActive: { backgroundColor: '#e74c3c' },
+  modeText: { fontSize: 12, color: '#7f8c8d' },
+  modeActiveText: { color: '#fff' },
+  status: { fontSize: 10, marginLeft: 'auto' },
+  newChatBtn: { marginTop: 16, backgroundColor: '#e74c3c', paddingHorizontal: 24, paddingVertical: 10, borderRadius: 8 },
+  newChatText: { color: '#fff', fontWeight: '600' },
+  messageList: { flex: 1, padding: 8 },
+  bubble: { padding: 10, borderRadius: 8, marginBottom: 6, maxWidth: '85%' },
+  userBubble: { alignSelf: 'flex-end', backgroundColor: '#2c3e5033' },
+  aiBubble: { alignSelf: 'flex-start', backgroundColor: '#16213e' },
+  bubbleText: { fontSize: 14 },
+  userText: { color: '#ecf0f1' },
+  aiText: { color: '#bdc3c7' },
+  inputBar: { flexDirection: 'row', padding: 8, backgroundColor: '#1a1a2e', alignItems: 'center' },
+  input: { flex: 1, height: 40, backgroundColor: '#1a1a2e', borderRadius: 8, borderWidth: 1, borderColor: '#2c3e50', paddingHorizontal: 12, color: '#ecf0f1', fontSize: 14 },
+  sendBtn: { width: 40, height: 40, backgroundColor: '#e74c3c', borderRadius: 8, justifyContent: 'center', alignItems: 'center', marginLeft: 6 },
+  sendText: { color: '#fff', fontSize: 18 },
+})

+ 33 - 0
packages/mobile/app.json

@@ -0,0 +1,33 @@
+{
+  "expo": {
+    "name": "KirinCode",
+    "slug": "kirincode-mobile",
+    "version": "0.4.0",
+    "orientation": "portrait",
+    "icon": "./assets/icon.png",
+    "userInterfaceStyle": "dark",
+    "backgroundColor": "#0f172a",
+    "splash": {
+      "backgroundColor": "#1a1a2e"
+    },
+    "ios": {
+      "supportsTablet": true,
+      "bundleIdentifier": "ai.kirincode.mobile",
+      "infoPlist": {
+        "NSAppTransportSecurity": {
+          "NSAllowsArbitraryLoads": true
+        }
+      }
+    },
+    "android": {
+      "adaptiveIcon": {
+        "backgroundColor": "#1a1a2e"
+      },
+      "package": "ai.kirincode.mobile"
+    },
+    "plugins": [
+      "expo-secure-store",
+      "expo-sqlite"
+    ]
+  }
+}

binární
packages/mobile/assets/icon.png


binární
packages/mobile/assets/icon_144.png


binární
packages/mobile/assets/icon_192.png


binární
packages/mobile/assets/icon_256.png


binární
packages/mobile/assets/icon_384.png


binární
packages/mobile/assets/icon_48.png


binární
packages/mobile/assets/icon_512.png


binární
packages/mobile/assets/icon_96.png


+ 7 - 0
packages/mobile/ios/ExportOptions.plist

@@ -0,0 +1,7 @@
+{
+  "method": "development",
+  "teamID": "",
+  "signingStyle": "automatic",
+  "stripSwiftSymbols": true,
+  "compileBitcode": false
+}

+ 1 - 1
packages/mobile/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/mobile",
-  "version": "0.4.0",
+  "version": "0.4.1",
   "private": true,
   "scripts": {
     "start": "expo start",

+ 9 - 0
packages/mobile/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "expo/tsconfig.base",
+  "compilerOptions": {
+    "strict": true,
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}