Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3d850eb
convert hey omi to a fastapi route
mdmohsin7 Aug 19, 2025
0e98dca
introduce subscription launch date for credit usage (#2826)
beastoin Aug 20, 2025
49d777b
Enhance the UI/UX of the chatgpt plugin (#2829)
beastoin Aug 21, 2025
01f6e8b
Khyfh docs (#2831)
beastoin Aug 21, 2025
03ffad4
feat: prompt user to review app after creating first conversation
krushnarout Aug 21, 2025
b546cca
Khdkc chat (#2833)
beastoin Aug 21, 2025
ead5b43
improve the BLE scan mechanism to reduce battery consumption by the m…
beastoin Aug 21, 2025
59461f6
fix: voice mode in desktop chat doesnt work
krushnarout Aug 21, 2025
39b0439
fix: select and copy the live transcript on desktop
krushnarout Aug 21, 2025
9487e0c
sort by due date
mdmohsin7 Aug 21, 2025
84a1890
convert hey omi app to a fastapi route (#2820)
mdmohsin7 Aug 21, 2025
39806cd
compress device images to fix memory issue
mdmohsin7 Aug 21, 2025
fbed848
use assetgen paths instead of hardcoded
mdmohsin7 Aug 21, 2025
1c6c3db
update typesense schema
mdmohsin7 Aug 21, 2025
f7f91b2
update typesense setup docs
mdmohsin7 Aug 21, 2025
25ee837
update typesense schema and docs (#2837)
mdmohsin7 Aug 21, 2025
a2038cc
feat: prompt user to review app after creating first conversation (#2…
aaravgarg Aug 21, 2025
7b4cfec
fix: voice mode in desktop chat does not work (#2834)
aaravgarg Aug 21, 2025
38358cb
sort action items by due date (#2835)
aaravgarg Aug 21, 2025
3cf1ba8
compress device images to fix memory issue (#2836)
aaravgarg Aug 21, 2025
441edca
Issue #2 - [UX] Submit button should be disabled when input field is …
Nikayel Dec 24, 2025
2011fe1
feat: Add scroll-to-top functionality when tapping current page icon …
Nikayel Dec 24, 2025
6062cfc
feat: Add database backup system with database name in notifications
Nikayel Jan 6, 2026
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
Binary file removed app/assets/images/omi-without-rope-turned-off.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/assets/images/omi-without-rope.png
Binary file not shown.
Binary file added app/assets/images/omi-without-rope.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed app/assets/images/onboarding-language-grey.png
Binary file not shown.
Binary file removed app/assets/images/onboarding-name-grey.png
Binary file not shown.
Binary file removed app/assets/images/onboarding-name-white.png
Binary file not shown.
Binary file removed app/assets/images/onboarding-name.png
Binary file not shown.
Binary file removed app/assets/images/onboarding-permissions.png
Binary file not shown.
14 changes: 7 additions & 7 deletions app/lib/backend/http/api/messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import 'package:http/http.dart' as http;
import 'package:path/path.dart';

Future<List<ServerMessage>> getMessagesServer({
String? pluginId,
String? appId,
bool dropdownSelected = false,
}) async {
if (pluginId == 'no_selected') pluginId = null;
if (appId == 'no_selected') appId = null;
// TODO: Add pagination
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v2/messages?plugin_id=${pluginId ?? ''}&dropdown_selected=$dropdownSelected',
url: '${Env.apiBaseUrl}v2/messages?app_id=${appId ?? ''}&dropdown_selected=$dropdownSelected',
headers: {},
method: 'GET',
body: '',
Expand All @@ -36,10 +36,10 @@ Future<List<ServerMessage>> getMessagesServer({
return [];
}

Future<List<ServerMessage>> clearChatServer({String? pluginId}) async {
if (pluginId == 'no_selected') pluginId = null;
Future<List<ServerMessage>> clearChatServer({String? appId}) async {
if (appId == 'no_selected') appId = null;
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v2/messages?plugin_id=${pluginId ?? ''}',
url: '${Env.apiBaseUrl}v2/messages?app_id=${appId ?? ''}',
headers: {},
method: 'DELETE',
body: '',
Expand Down Expand Up @@ -77,7 +77,7 @@ ServerMessageChunk? parseMessageChunk(String line, String messageId) {
}

Stream<ServerMessageChunk> sendMessageStreamServer(String text, {String? appId, List<String>? filesId}) async* {
var url = '${Env.apiBaseUrl}v2/messages?plugin_id=$appId';
var url = '${Env.apiBaseUrl}v2/messages?app_id=$appId';
if (appId == null || appId.isEmpty || appId == 'null' || appId == 'no_selected') {
url = '${Env.apiBaseUrl}v2/messages';
}
Expand Down
160 changes: 108 additions & 52 deletions app/lib/desktop/pages/chat/widgets/desktop_voice_recorder_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import 'dart:math' as math;
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:omi/backend/http/api/messages.dart';
import 'package:omi/services/services.dart';
import 'package:omi/utils/alerts/app_snackbar.dart';
import 'package:omi/utils/file.dart';
import 'package:omi/utils/responsive/responsive_helper.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shimmer/shimmer.dart';
import 'package:omi/ui/atoms/omi_icon_button.dart';

Expand Down Expand Up @@ -45,6 +45,9 @@ class _DesktopVoiceRecorderWidgetState extends State<DesktopVoiceRecorderWidget>
late AnimationController _animationController;
Timer? _waveformTimer;

// Platform channel for desktop permissions
static const MethodChannel _screenCaptureChannel = MethodChannel('screenCapturePlatform');

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -72,71 +75,120 @@ class _DesktopVoiceRecorderWidgetState extends State<DesktopVoiceRecorderWidget>

// Make sure to stop recording when widget is disposed
if (_state == RecordingState.recording) {
ServiceManager.instance().mic.stop();
ServiceManager.instance().systemAudio.stop();
}

super.dispose();
}

Future<bool> _checkAndRequestMicrophonePermission() async {
try {
// Check microphone permission first
String micStatus = await _screenCaptureChannel.invokeMethod('checkMicrophonePermission');

if (micStatus != 'granted') {
if (micStatus == 'undetermined' || micStatus == 'unavailable') {
bool micGranted = await _screenCaptureChannel.invokeMethod('requestMicrophonePermission');
if (!micGranted) {
AppSnackbar.showSnackbarError('Microphone permission is required for voice recording.');
return false;
}
} else if (micStatus == 'denied') {
AppSnackbar.showSnackbarError(
'Microphone permission denied. Please grant permission in System Preferences > Privacy & Security > Microphone.');
return false;
}
}
return true;
} catch (e) {
AppSnackbar.showSnackbarError('Failed to check Microphone permission: $e');
return false;
}
}

Future<void> _startRecording() async {
await Permission.microphone.request();
// Check and request microphone permission using desktop platform channel
if (!await _checkAndRequestMicrophonePermission()) {
setState(() {
_state = RecordingState.transcribeFailed;
});
return;
}

await ServiceManager.instance().mic.start(onByteReceived: (bytes) {
if (_state == RecordingState.recording && mounted) {
if (mounted) {
setState(() {
_audioChunks.add(bytes.toList());
await ServiceManager.instance().systemAudio.start(
onByteReceived: (bytes) {
if (_state == RecordingState.recording && mounted) {
if (mounted) {
setState(() {
_audioChunks.add(bytes.toList());

if (bytes.isNotEmpty) {
double rms = 0;
if (bytes.isNotEmpty) {
double rms = 0;

for (int i = 0; i < bytes.length - 1; i += 2) {
int sample = bytes[i] | (bytes[i + 1] << 8);
for (int i = 0; i < bytes.length - 1; i += 2) {
int sample = bytes[i] | (bytes[i + 1] << 8);

if (sample > 32767) {
sample = sample - 65536;
if (sample > 32767) {
sample = sample - 65536;
}

rms += sample * sample;
}

rms += sample * sample;
}
int sampleCount = bytes.length ~/ 2;
if (sampleCount > 0) {
rms = math.sqrt(rms / sampleCount) / 32768.0;
} else {
rms = 0;
}

int sampleCount = bytes.length ~/ 2;
if (sampleCount > 0) {
rms = math.sqrt(rms / sampleCount) / 32768.0;
} else {
rms = 0;
}
final level = math.pow(rms, 0.4).toDouble().clamp(0.1, 1.0);

final level = math.pow(rms, 0.4).toDouble().clamp(0.1, 1.0);
for (int i = 0; i < _audioLevels.length - 1; i++) {
_audioLevels[i] = _audioLevels[i + 1];
}

for (int i = 0; i < _audioLevels.length - 1; i++) {
_audioLevels[i] = _audioLevels[i + 1];
_audioLevels[_audioLevels.length - 1] = level;
}

_audioLevels[_audioLevels.length - 1] = level;
}
});
});
}
}
}
}, onRecording: () {
debugPrint('Recording started');
setState(() {
_state = RecordingState.recording;
_audioChunks = [];
for (int i = 0; i < _audioLevels.length; i++) {
_audioLevels[i] = 0.1;
}
});
}, onStop: () {
debugPrint('Recording stopped');
}, onInitializing: () {
debugPrint('Initializing');
});
},
onFormatReceived: (format) {
debugPrint('Audio format received: $format');
},
onRecording: () {
debugPrint('Recording started');
setState(() {
_state = RecordingState.recording;
_audioChunks = [];
for (int i = 0; i < _audioLevels.length; i++) {
_audioLevels[i] = 0.1;
}
});
},
onStop: () {
debugPrint('Recording stopped');
},
onError: (error) {
debugPrint('Recording error: $error');
setState(() {
_state = RecordingState.transcribeFailed;
});
},
);
}

Future<void> _stopRecording() async {
_waveformTimer?.cancel();
ServiceManager.instance().mic.stop();
ServiceManager.instance().systemAudio.stop();
}

void _cancelRecording() {
// Stop recording and close widget without processing
_waveformTimer?.cancel();
ServiceManager.instance().systemAudio.stop();
widget.onClose();
}

Future<void> _processRecording() async {
Expand Down Expand Up @@ -220,7 +272,7 @@ class _DesktopVoiceRecorderWidgetState extends State<DesktopVoiceRecorderWidget>
size: 32,
iconSize: 14,
borderRadius: 8,
onPressed: widget.onClose,
onPressed: _cancelRecording,
),
),
Expanded(
Expand Down Expand Up @@ -346,12 +398,16 @@ class _DesktopVoiceRecorderWidgetState extends State<DesktopVoiceRecorderWidget>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Error',
style: TextStyle(
color: Colors.redAccent,
fontSize: 14,
fontWeight: FontWeight.w600,
const Flexible(
child: Text(
'Transcription failed',
style: TextStyle(
color: Colors.redAccent,
fontSize: 12,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
const SizedBox(width: 16),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class _DesktopRecordingWidgetState extends State<DesktopRecordingWidget> {
),
),
const SizedBox(height: 6),
Text(
SelectableText(
isInitializing
? 'Preparing system audio capture'
: 'Click the button above to begin capturing audio and create live transcripts',
Expand Down Expand Up @@ -627,7 +627,7 @@ class _DesktopRecordingWidgetState extends State<DesktopRecordingWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Original text
Text(
SelectableText(
_tryDecodingText(segment.text.trim()),
style: const TextStyle(
fontSize: 14,
Expand All @@ -640,7 +640,7 @@ class _DesktopRecordingWidgetState extends State<DesktopRecordingWidget> {
const SizedBox(height: 6),
...segment.translations.map((translation) => Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
child: SelectableText(
_tryDecodingText(translation.text),
style: const TextStyle(
fontSize: 14,
Expand Down Expand Up @@ -687,7 +687,7 @@ class _DesktopRecordingWidgetState extends State<DesktopRecordingWidget> {
color: ResponsiveHelper.textTertiary,
),
SizedBox(width: 4),
Text(
SelectableText(
'translated by omi',
style: TextStyle(
fontSize: 10,
Expand Down
33 changes: 4 additions & 29 deletions app/lib/gen/assets.gen.dart

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

12 changes: 11 additions & 1 deletion app/lib/pages/action_items/action_items_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class _ActionItemsPageState extends State<ActionItemsPage> with AutomaticKeepAli
await _appReviewService.markFirstActionItemCompleted();

if (mounted) {
await _appReviewService.showReviewPromptIfNeeded(context);
await _appReviewService.showReviewPromptIfNeeded(context, isProcessingFirstConversation: false);
}
}
}
Expand All @@ -87,6 +87,16 @@ class _ActionItemsPageState extends State<ActionItemsPage> with AutomaticKeepAli
}
}

void scrollToTop() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}

void _showCreateActionItemSheet() {
showModalBottomSheet(
context: context,
Expand Down
Loading