mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 15:24:35 +01:00
feat: ask ai (#936)
* feat: ask ai in ssh terminal Fixes #934 * new(ask_ai): settings * fix: app hot reload * new: l10n * chore: deps. * opt.
This commit is contained in:
1
.github/workflows/analysis.yml
vendored
1
.github/workflows/analysis.yml
vendored
@@ -32,7 +32,6 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
${{ env.PUB_CACHE }}
|
${{ env.PUB_CACHE }}
|
||||||
~/.pub-cache
|
~/.pub-cache
|
||||||
.dart_tool/package_config.json
|
|
||||||
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-${{ hashFiles('**/pubspec.yaml') }}
|
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-${{ hashFiles('**/pubspec.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-
|
${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-
|
||||||
|
|||||||
11
lib/app.dart
11
lib/app.dart
@@ -11,9 +11,16 @@ import 'package:server_box/view/page/home.dart';
|
|||||||
|
|
||||||
part 'intro.dart';
|
part 'intro.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatefulWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
late final Future<List<IntroPageBuilder>> _introFuture = _IntroPage.builders;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_setup(context);
|
_setup(context);
|
||||||
@@ -91,7 +98,7 @@ class MyApp extends StatelessWidget {
|
|||||||
theme: light.fixWindowsFont,
|
theme: light.fixWindowsFont,
|
||||||
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
||||||
home: FutureBuilder<List<IntroPageBuilder>>(
|
home: FutureBuilder<List<IntroPageBuilder>>(
|
||||||
future: _IntroPage.builders,
|
future: _introFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
context.setLibL10n();
|
context.setLibL10n();
|
||||||
final appL10n = AppLocalizations.of(context);
|
final appL10n = AppLocalizations.of(context);
|
||||||
|
|||||||
74
lib/data/model/ai/ask_ai_models.dart
Normal file
74
lib/data/model/ai/ask_ai_models.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Chat message exchanged with the Ask AI service.
|
||||||
|
enum AskAiMessageRole { user, assistant }
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class AskAiMessage {
|
||||||
|
const AskAiMessage({
|
||||||
|
required this.role,
|
||||||
|
required this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AskAiMessageRole role;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
String get apiRole {
|
||||||
|
switch (role) {
|
||||||
|
case AskAiMessageRole.user:
|
||||||
|
return 'user';
|
||||||
|
case AskAiMessageRole.assistant:
|
||||||
|
return 'assistant';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recommended command returned by the AI tool call.
|
||||||
|
@immutable
|
||||||
|
class AskAiCommand {
|
||||||
|
const AskAiCommand({
|
||||||
|
required this.command,
|
||||||
|
this.description = '',
|
||||||
|
this.toolName,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String command;
|
||||||
|
final String description;
|
||||||
|
final String? toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
sealed class AskAiEvent {
|
||||||
|
const AskAiEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Incremental text delta emitted while streaming the AI response.
|
||||||
|
class AskAiContentDelta extends AskAiEvent {
|
||||||
|
const AskAiContentDelta(this.delta);
|
||||||
|
final String delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits when a tool call returns a runnable command suggestion.
|
||||||
|
class AskAiToolSuggestion extends AskAiEvent {
|
||||||
|
const AskAiToolSuggestion(this.command);
|
||||||
|
final AskAiCommand command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals that the stream finished successfully.
|
||||||
|
class AskAiCompleted extends AskAiEvent {
|
||||||
|
const AskAiCompleted({
|
||||||
|
required this.fullText,
|
||||||
|
required this.commands,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String fullText;
|
||||||
|
final List<AskAiCommand> commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals that the stream terminated with an error before completion.
|
||||||
|
class AskAiStreamError extends AskAiEvent {
|
||||||
|
const AskAiStreamError(this.error, this.stackTrace);
|
||||||
|
|
||||||
|
final Object error;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
}
|
||||||
346
lib/data/provider/ai/ask_ai.dart
Normal file
346
lib/data/provider/ai/ask_ai.dart
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:server_box/data/model/ai/ask_ai_models.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/store/setting.dart';
|
||||||
|
|
||||||
|
final askAiRepositoryProvider = Provider<AskAiRepository>((ref) {
|
||||||
|
return AskAiRepository();
|
||||||
|
});
|
||||||
|
|
||||||
|
class AskAiRepository {
|
||||||
|
AskAiRepository({Dio? dio}) : _dio = dio ?? Dio();
|
||||||
|
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
SettingStore get _settings => Stores.setting;
|
||||||
|
|
||||||
|
/// Streams the AI response using the configured endpoint.
|
||||||
|
Stream<AskAiEvent> ask({
|
||||||
|
required String selection,
|
||||||
|
String? localeHint,
|
||||||
|
List<AskAiMessage> conversation = const [],
|
||||||
|
}) async* {
|
||||||
|
final baseUrl = _settings.askAiBaseUrl.fetch().trim();
|
||||||
|
final apiKey = _settings.askAiApiKey.fetch().trim();
|
||||||
|
final model = _settings.askAiModel.fetch().trim();
|
||||||
|
|
||||||
|
final missing = <AskAiConfigField>[];
|
||||||
|
if (baseUrl.isEmpty) missing.add(AskAiConfigField.baseUrl);
|
||||||
|
if (apiKey.isEmpty) missing.add(AskAiConfigField.apiKey);
|
||||||
|
if (model.isEmpty) missing.add(AskAiConfigField.model);
|
||||||
|
if (missing.isNotEmpty) {
|
||||||
|
throw AskAiConfigException(missingFields: missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedBaseUri = Uri.tryParse(baseUrl);
|
||||||
|
final hasScheme = parsedBaseUri?.hasScheme ?? false;
|
||||||
|
final hasHost = (parsedBaseUri?.host ?? '').isNotEmpty;
|
||||||
|
if (!hasScheme || !hasHost) {
|
||||||
|
throw AskAiConfigException(invalidBaseUrl: baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
final uri = _composeUri(baseUrl, '/v1/chat/completions');
|
||||||
|
final authHeader = apiKey.startsWith('Bearer ') ? apiKey : 'Bearer $apiKey';
|
||||||
|
final headers = <String, String>{
|
||||||
|
Headers.acceptHeader: 'text/event-stream',
|
||||||
|
Headers.contentTypeHeader: Headers.jsonContentType,
|
||||||
|
'Authorization': authHeader,
|
||||||
|
};
|
||||||
|
|
||||||
|
final requestBody = _buildRequestBody(
|
||||||
|
model: model,
|
||||||
|
selection: selection,
|
||||||
|
localeHint: localeHint,
|
||||||
|
conversation: conversation,
|
||||||
|
);
|
||||||
|
|
||||||
|
Response<ResponseBody> response;
|
||||||
|
try {
|
||||||
|
response = await _dio.postUri<ResponseBody>(
|
||||||
|
uri,
|
||||||
|
data: jsonEncode(requestBody),
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
headers: headers,
|
||||||
|
sendTimeout: const Duration(seconds: 20),
|
||||||
|
receiveTimeout: const Duration(minutes: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw AskAiNetworkException(message: e.message ?? 'Request failed', cause: e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = response.data;
|
||||||
|
if (body == null) {
|
||||||
|
throw AskAiNetworkException(message: 'Empty response body');
|
||||||
|
}
|
||||||
|
|
||||||
|
final contentBuffer = StringBuffer();
|
||||||
|
final commands = <AskAiCommand>[];
|
||||||
|
final toolBuilders = <int, _ToolCallBuilder>{};
|
||||||
|
final utf8Stream = body.stream.cast<List<int>>().transform(utf8.decoder);
|
||||||
|
final carry = StringBuffer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await for (final chunk in utf8Stream) {
|
||||||
|
carry.write(chunk);
|
||||||
|
final segments = carry.toString().split('\n\n');
|
||||||
|
carry
|
||||||
|
..clear()
|
||||||
|
..write(segments.removeLast());
|
||||||
|
|
||||||
|
for (final segment in segments) {
|
||||||
|
final lines = segment.split('\n');
|
||||||
|
for (final rawLine in lines) {
|
||||||
|
final line = rawLine.trim();
|
||||||
|
if (line.isEmpty || !line.startsWith('data:')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final payload = line.substring(5).trim();
|
||||||
|
if (payload.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (payload == '[DONE]') {
|
||||||
|
yield AskAiCompleted(
|
||||||
|
fullText: contentBuffer.toString(),
|
||||||
|
commands: List.unmodifiable(commands),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> json;
|
||||||
|
try {
|
||||||
|
json = jsonDecode(payload) as Map<String, dynamic>;
|
||||||
|
} catch (e, s) {
|
||||||
|
yield AskAiStreamError(e, s);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final choices = json['choices'];
|
||||||
|
if (choices is! List || choices.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final choice in choices) {
|
||||||
|
if (choice is! Map<String, dynamic>) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final delta = choice['delta'];
|
||||||
|
if (delta is Map<String, dynamic>) {
|
||||||
|
final content = delta['content'];
|
||||||
|
if (content is String && content.isNotEmpty) {
|
||||||
|
contentBuffer.write(content);
|
||||||
|
yield AskAiContentDelta(content);
|
||||||
|
} else if (content is List) {
|
||||||
|
for (final item in content) {
|
||||||
|
if (item is Map<String, dynamic>) {
|
||||||
|
final text = item['text'] as String?;
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
contentBuffer.write(text);
|
||||||
|
yield AskAiContentDelta(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final toolCalls = delta['tool_calls'];
|
||||||
|
if (toolCalls is List) {
|
||||||
|
for (final toolCall in toolCalls) {
|
||||||
|
if (toolCall is! Map<String, dynamic>) continue;
|
||||||
|
final index = toolCall['index'] as int? ?? 0;
|
||||||
|
final builder = toolBuilders.putIfAbsent(index, _ToolCallBuilder.new);
|
||||||
|
final function = toolCall['function'];
|
||||||
|
if (function is Map<String, dynamic>) {
|
||||||
|
builder.name ??= function['name'] as String?;
|
||||||
|
final args = function['arguments'] as String?;
|
||||||
|
if (args != null && args.isNotEmpty) {
|
||||||
|
builder.arguments.write(args);
|
||||||
|
final command = builder.tryBuild();
|
||||||
|
if (command != null) {
|
||||||
|
commands.add(command);
|
||||||
|
yield AskAiToolSuggestion(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final finishReason = choice['finish_reason'];
|
||||||
|
if (finishReason == 'tool_calls') {
|
||||||
|
for (final builder in toolBuilders.values) {
|
||||||
|
final command = builder.tryBuild(force: true);
|
||||||
|
if (command != null) {
|
||||||
|
commands.add(command);
|
||||||
|
yield AskAiToolSuggestion(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toolBuilders.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush remaining buffer if [DONE] not received.
|
||||||
|
if (contentBuffer.isNotEmpty || commands.isNotEmpty) {
|
||||||
|
yield AskAiCompleted(
|
||||||
|
fullText: contentBuffer.toString(),
|
||||||
|
commands: List.unmodifiable(commands),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
yield AskAiStreamError(e, s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildRequestBody({
|
||||||
|
required String model,
|
||||||
|
required String selection,
|
||||||
|
required List<AskAiMessage> conversation,
|
||||||
|
String? localeHint,
|
||||||
|
}) {
|
||||||
|
final promptBuffer = StringBuffer()
|
||||||
|
..writeln('你是一个 SSH 终端助手。')
|
||||||
|
..writeln('用户会提供一段终端输出或命令,请结合上下文给出解释。')
|
||||||
|
..writeln('当需要给出可执行命令时,调用 `recommend_shell` 工具,并提供简短描述。')
|
||||||
|
..writeln('仅在非常确定命令安全时才给出建议。');
|
||||||
|
|
||||||
|
if (localeHint != null && localeHint.isNotEmpty) {
|
||||||
|
promptBuffer
|
||||||
|
..writeln('请优先使用用户的语言输出:$localeHint。')
|
||||||
|
..writeln('如果无法判断语言,请使用简体中文。');
|
||||||
|
} else {
|
||||||
|
promptBuffer.writeln('如果无法判断语言,请使用简体中文。');
|
||||||
|
}
|
||||||
|
|
||||||
|
final messages = <Map<String, String>>[
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content': promptBuffer.toString(),
|
||||||
|
},
|
||||||
|
...conversation.map((message) => {
|
||||||
|
'role': message.apiRole,
|
||||||
|
'content': message.content,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': '以下是终端选中的内容:\n$selection',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model': model,
|
||||||
|
'stream': true,
|
||||||
|
'messages': messages,
|
||||||
|
'tools': [
|
||||||
|
{
|
||||||
|
'type': 'function',
|
||||||
|
'function': {
|
||||||
|
'name': 'recommend_shell',
|
||||||
|
'description': '返回一个用户可以直接复制执行的终端命令。',
|
||||||
|
'parameters': {
|
||||||
|
'type': 'object',
|
||||||
|
'required': ['command'],
|
||||||
|
'properties': {
|
||||||
|
'command': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '完整的终端命令,确保可以被粘贴后直接执行。',
|
||||||
|
},
|
||||||
|
'description': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '简述该命令的作用或注意事项。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri _composeUri(String base, String path) {
|
||||||
|
final sanitizedBase = base.replaceAll(RegExp(r'/+$'), '');
|
||||||
|
final sanitizedPath = path.replaceFirst(RegExp(r'^/+'), '');
|
||||||
|
return Uri.parse('$sanitizedBase/$sanitizedPath');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToolCallBuilder {
|
||||||
|
_ToolCallBuilder();
|
||||||
|
|
||||||
|
final StringBuffer arguments = StringBuffer();
|
||||||
|
String? name;
|
||||||
|
bool _emitted = false;
|
||||||
|
|
||||||
|
AskAiCommand? tryBuild({bool force = false}) {
|
||||||
|
if (_emitted && !force) return null;
|
||||||
|
final raw = arguments.toString();
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
|
final command = decoded['command'] as String?;
|
||||||
|
if (command == null || command.trim().isEmpty) {
|
||||||
|
if (force) {
|
||||||
|
_emitted = true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final description = decoded['description'] as String? ?? decoded['explanation'] as String? ?? '';
|
||||||
|
_emitted = true;
|
||||||
|
return AskAiCommand(
|
||||||
|
command: command.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
toolName: name,
|
||||||
|
);
|
||||||
|
} on FormatException {
|
||||||
|
if (force) {
|
||||||
|
_emitted = true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
enum AskAiConfigField { baseUrl, apiKey, model }
|
||||||
|
|
||||||
|
class AskAiConfigException implements Exception {
|
||||||
|
const AskAiConfigException({this.missingFields = const [], this.invalidBaseUrl});
|
||||||
|
|
||||||
|
final List<AskAiConfigField> missingFields;
|
||||||
|
final String? invalidBaseUrl;
|
||||||
|
|
||||||
|
bool get hasInvalidBaseUrl => (invalidBaseUrl ?? '').isNotEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final parts = <String>[];
|
||||||
|
if (missingFields.isNotEmpty) {
|
||||||
|
parts.add('missing: ${missingFields.map((e) => e.name).join(', ')}');
|
||||||
|
}
|
||||||
|
if (hasInvalidBaseUrl) {
|
||||||
|
parts.add('invalidBaseUrl: $invalidBaseUrl');
|
||||||
|
}
|
||||||
|
if (parts.isEmpty) {
|
||||||
|
return 'AskAiConfigException()';
|
||||||
|
}
|
||||||
|
return 'AskAiConfigException(${parts.join('; ')})';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class AskAiNetworkException implements Exception {
|
||||||
|
const AskAiNetworkException({required this.message, this.cause});
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final Object? cause;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AskAiNetworkException(message: $message)';
|
||||||
|
}
|
||||||
@@ -142,6 +142,11 @@ class SettingStore extends HiveStore {
|
|||||||
/// Whether collapse UI items by default
|
/// Whether collapse UI items by default
|
||||||
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
||||||
|
|
||||||
|
/// Terminal AI helper configuration
|
||||||
|
late final askAiBaseUrl = propertyDefault('askAiBaseUrl', 'https://api.openai.com');
|
||||||
|
late final askAiApiKey = propertyDefault('askAiApiKey', '');
|
||||||
|
late final askAiModel = propertyDefault('askAiModel', 'gpt-4o-mini');
|
||||||
|
|
||||||
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
|
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
|
||||||
|
|
||||||
/// Docker is more popular than podman, set to `false` to use docker
|
/// Docker is more popular than podman, set to `false` to use docker
|
||||||
|
|||||||
@@ -1747,6 +1747,102 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
||||||
String get writeScriptTip;
|
String get writeScriptTip;
|
||||||
|
|
||||||
|
/// No description provided for @askAi.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask AI'**
|
||||||
|
String get askAi;
|
||||||
|
|
||||||
|
/// No description provided for @askAiUsageHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Used in SSH Terminal'**
|
||||||
|
String get askAiUsageHint;
|
||||||
|
|
||||||
|
/// No description provided for @askAiBaseUrl.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Base URL'**
|
||||||
|
String get askAiBaseUrl;
|
||||||
|
|
||||||
|
/// No description provided for @askAiModel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Model'**
|
||||||
|
String get askAiModel;
|
||||||
|
|
||||||
|
/// No description provided for @askAiApiKey.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'API Key'**
|
||||||
|
String get askAiApiKey;
|
||||||
|
|
||||||
|
/// No description provided for @askAiConfigMissing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Please configure {fields} in Settings.'**
|
||||||
|
String askAiConfigMissing(String fields);
|
||||||
|
|
||||||
|
/// No description provided for @askAiConfirmExecute.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Confirm before executing'**
|
||||||
|
String get askAiConfirmExecute;
|
||||||
|
|
||||||
|
/// No description provided for @askAiCommandInserted.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Command inserted into terminal'**
|
||||||
|
String get askAiCommandInserted;
|
||||||
|
|
||||||
|
/// No description provided for @askAiAwaitingResponse.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Waiting for AI response...'**
|
||||||
|
String get askAiAwaitingResponse;
|
||||||
|
|
||||||
|
/// No description provided for @askAiNoResponse.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No response'**
|
||||||
|
String get askAiNoResponse;
|
||||||
|
|
||||||
|
/// No description provided for @askAiRecommendedCommand.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'AI suggested command'**
|
||||||
|
String get askAiRecommendedCommand;
|
||||||
|
|
||||||
|
/// No description provided for @askAiInsertTerminal.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Insert into terminal'**
|
||||||
|
String get askAiInsertTerminal;
|
||||||
|
|
||||||
|
/// No description provided for @askAiSelectedContent.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Selected content'**
|
||||||
|
String get askAiSelectedContent;
|
||||||
|
|
||||||
|
/// No description provided for @askAiConversation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'AI conversation'**
|
||||||
|
String get askAiConversation;
|
||||||
|
|
||||||
|
/// No description provided for @askAiFollowUpHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask a follow-up...'**
|
||||||
|
String get askAiFollowUpHint;
|
||||||
|
|
||||||
|
/// No description provided for @askAiSend.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Send'**
|
||||||
|
String get askAiSend;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -923,4 +923,54 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'KI fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Verwendet im SSH-Terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Basis-URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modell';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API-Schlüssel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Bitte konfigurieren Sie $fields in den Einstellungen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Vor Ausführung bestätigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Befehl ins Terminal eingefügt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Warte auf KI-Antwort...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Keine Antwort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'KI-empfohlener Befehl';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'In Terminal einfügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Ausgewählter Inhalt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'KI-Unterhaltung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Weitere Frage stellen...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Senden';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -914,4 +914,54 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Ask AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Used in SSH Terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Base URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API Key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Please configure $fields in Settings.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirm before executing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Command inserted into terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Waiting for AI response...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'No response';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI suggested command';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Insert into terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Selected content';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI conversation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Ask a follow-up...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Send';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -925,4 +925,54 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Preguntar a la IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Usado en el terminal SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Clave API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Configura $fields en Ajustes.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirmar antes de ejecutar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Comando insertado en el terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Esperando la respuesta de la IA...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Sin respuesta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Comando sugerido por la IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Insertar en el terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Contenido seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Conversación con la IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Haz una pregunta adicional...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Enviar';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -928,4 +928,54 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
|
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Demander à l\'IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Utilisé dans le terminal SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL de base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modèle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Clé API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Veuillez configurer $fields dans les paramètres.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirmer avant d\'exécuter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Commande insérée dans le terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'En attente de la réponse de l\'IA...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Aucune réponse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Commande suggérée par l\'IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Insérer dans le terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Contenu sélectionné';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Conversation avec l\'IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Poser une question supplémentaire...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Envoyer';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -915,4 +915,54 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Tanya AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Digunakan di Terminal SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL dasar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Kunci API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Harap konfigurasikan $fields di Pengaturan.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Konfirmasi sebelum menjalankan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Perintah dimasukkan ke terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Menunggu respons AI...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Tidak ada respons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Perintah yang disarankan AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Masukkan ke terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Konten yang dipilih';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Percakapan AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Ajukan pertanyaan lanjutan...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Kirim';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -885,4 +885,54 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'AI に質問';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'SSH ターミナルで使用';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'ベース URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'モデル';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API キー';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return '設定で $fields を構成してください。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => '実行前に確認';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'コマンドをターミナルに挿入しました';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'AI の応答を待機中...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => '応答なし';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI 推奨コマンド';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'ターミナルに挿入';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => '選択した内容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI 会話';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => '追質問をする...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => '送信';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -922,4 +922,54 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'AI vragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Gebruikt in de SSH-terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Basis-URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API-sleutel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Configureer $fields in de instellingen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Bevestigen voor uitvoeren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Commando in terminal ingevoegd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Wachten op AI-reactie...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Geen reactie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Door AI voorgestelde opdracht';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'In terminal invoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Geselecteerde inhoud';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI-gesprek';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Stel een vervolgvraag...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Verzenden';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -917,4 +917,54 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Perguntar à IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Usado no terminal SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Chave de API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Configure $fields nas configurações.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirmar antes de executar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Comando inserido no terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Aguardando resposta da IA...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Sem resposta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Comando sugerido pela IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Inserir no terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Conteúdo selecionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Conversa com a IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Faça uma pergunta adicional...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Enviar';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -920,4 +920,54 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Спросить ИИ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Используется в SSH-терминале';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Базовый URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Ключ API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Настройте $fields в настройках.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Подтвердите перед выполнением';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Команда вставлена в терминал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Ожидание ответа ИИ...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Нет ответа';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Команда, предложенная ИИ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Вставить в терминал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Выбранное содержимое';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Разговор с ИИ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Задайте дополнительный вопрос...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Отправить';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -915,4 +915,54 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Yapay zekaya sor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'SSH Terminalinde kullanılır';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Temel URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API anahtarı';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Lütfen Ayarlar\'da $fields öğesini yapılandırın.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Çalıştırmadan önce onayla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Komut terminale eklendi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Yapay zekâ yanıtı bekleniyor...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Yanıt yok';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'YZ önerilen komut';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Terminale ekle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Seçilen içerik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'YZ sohbeti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Yeni bir soru sor...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Gönder';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -921,4 +921,54 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Запитати ШІ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Використовується в SSH-терміналі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Базова URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Ключ API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return 'Налаштуйте $fields у налаштуваннях.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Підтвердити перед виконанням';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Команду вставлено в термінал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Очікування відповіді ШІ...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Відповідь відсутня';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Команда, запропонована ШІ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Вставити в термінал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Вибраний вміст';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Розмова з ШІ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Поставте додаткове запитання...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => 'Надіслати';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -870,6 +870,56 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => '问 AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => '用于 SSH 终端';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => '基础 URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => '模型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API 密钥';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return '请前往设置配置 $fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => '执行前确认';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => '命令已插入终端';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => '等待 AI 响应...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => '无回复内容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI 推荐命令';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => '插入终端';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => '选中的内容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI 对话';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => '继续提问...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => '发送';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||||
@@ -1738,4 +1788,54 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => '詢問 AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => '於 SSH 終端機中使用';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => '基礎 URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => '模型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API 金鑰';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(String fields) {
|
||||||
|
return '請前往設定配置 $fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => '執行前確認';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => '指令已插入終端機';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => '等待 AI 回應...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => '無回覆內容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI 推薦指令';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => '插入終端機';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => '選取的內容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI 對話';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => '繼續提問...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSend => '傳送';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
||||||
"write": "Schreiben",
|
"write": "Schreiben",
|
||||||
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
||||||
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
|
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
|
||||||
}
|
"askAi": "KI fragen",
|
||||||
|
"askAiUsageHint": "Verwendet im SSH-Terminal",
|
||||||
|
"askAiBaseUrl": "Basis-URL",
|
||||||
|
"askAiModel": "Modell",
|
||||||
|
"askAiApiKey": "API-Schlüssel",
|
||||||
|
"askAiConfigMissing": "Bitte konfigurieren Sie {fields} in den Einstellungen.",
|
||||||
|
"askAiConfirmExecute": "Vor Ausführung bestätigen",
|
||||||
|
"askAiCommandInserted": "Befehl ins Terminal eingefügt",
|
||||||
|
"askAiAwaitingResponse": "Warte auf KI-Antwort...",
|
||||||
|
"askAiNoResponse": "Keine Antwort",
|
||||||
|
"askAiRecommendedCommand": "KI-empfohlener Befehl",
|
||||||
|
"askAiInsertTerminal": "In Terminal einfügen",
|
||||||
|
"askAiSelectedContent": "Ausgewählter Inhalt",
|
||||||
|
"askAiConversation": "KI-Unterhaltung",
|
||||||
|
"askAiFollowUpHint": "Weitere Frage stellen...",
|
||||||
|
"askAiSend": "Senden"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,28 @@
|
|||||||
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
||||||
"write": "Write",
|
"write": "Write",
|
||||||
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
||||||
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content."
|
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.",
|
||||||
}
|
"@askAiConfigMissing": {
|
||||||
|
"placeholders": {
|
||||||
|
"fields": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"askAi": "Ask AI",
|
||||||
|
"askAiUsageHint": "Used in SSH Terminal",
|
||||||
|
"askAiBaseUrl": "Base URL",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiApiKey": "API Key",
|
||||||
|
"askAiConfigMissing": "Please configure {fields} in Settings.",
|
||||||
|
"askAiConfirmExecute": "Confirm before executing",
|
||||||
|
"askAiCommandInserted": "Command inserted into terminal",
|
||||||
|
"askAiAwaitingResponse": "Waiting for AI response...",
|
||||||
|
"askAiNoResponse": "No response",
|
||||||
|
"askAiRecommendedCommand": "AI suggested command",
|
||||||
|
"askAiInsertTerminal": "Insert into terminal",
|
||||||
|
"askAiSelectedContent": "Selected content",
|
||||||
|
"askAiConversation": "AI conversation",
|
||||||
|
"askAiFollowUpHint": "Ask a follow-up...",
|
||||||
|
"askAiSend": "Send"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
||||||
"write": "Escribir",
|
"write": "Escribir",
|
||||||
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
||||||
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script."
|
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
|
||||||
}
|
"askAi": "Preguntar a la IA",
|
||||||
|
"askAiUsageHint": "Usado en el terminal SSH",
|
||||||
|
"askAiBaseUrl": "URL base",
|
||||||
|
"askAiModel": "Modelo",
|
||||||
|
"askAiApiKey": "Clave API",
|
||||||
|
"askAiConfigMissing": "Configura {fields} en Ajustes.",
|
||||||
|
"askAiConfirmExecute": "Confirmar antes de ejecutar",
|
||||||
|
"askAiCommandInserted": "Comando insertado en el terminal",
|
||||||
|
"askAiAwaitingResponse": "Esperando la respuesta de la IA...",
|
||||||
|
"askAiNoResponse": "Sin respuesta",
|
||||||
|
"askAiRecommendedCommand": "Comando sugerido por la IA",
|
||||||
|
"askAiInsertTerminal": "Insertar en el terminal",
|
||||||
|
"askAiSelectedContent": "Contenido seleccionado",
|
||||||
|
"askAiConversation": "Conversación con la IA",
|
||||||
|
"askAiFollowUpHint": "Haz una pregunta adicional...",
|
||||||
|
"askAiSend": "Enviar"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
||||||
"write": "Écrire",
|
"write": "Écrire",
|
||||||
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
||||||
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script."
|
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.",
|
||||||
}
|
"askAi": "Demander à l'IA",
|
||||||
|
"askAiUsageHint": "Utilisé dans le terminal SSH",
|
||||||
|
"askAiBaseUrl": "URL de base",
|
||||||
|
"askAiModel": "Modèle",
|
||||||
|
"askAiApiKey": "Clé API",
|
||||||
|
"askAiConfigMissing": "Veuillez configurer {fields} dans les paramètres.",
|
||||||
|
"askAiConfirmExecute": "Confirmer avant d'exécuter",
|
||||||
|
"askAiCommandInserted": "Commande insérée dans le terminal",
|
||||||
|
"askAiAwaitingResponse": "En attente de la réponse de l'IA...",
|
||||||
|
"askAiNoResponse": "Aucune réponse",
|
||||||
|
"askAiRecommendedCommand": "Commande suggérée par l'IA",
|
||||||
|
"askAiInsertTerminal": "Insérer dans le terminal",
|
||||||
|
"askAiSelectedContent": "Contenu sélectionné",
|
||||||
|
"askAiConversation": "Conversation avec l'IA",
|
||||||
|
"askAiFollowUpHint": "Poser une question supplémentaire...",
|
||||||
|
"askAiSend": "Envoyer"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
||||||
"write": "Tulis",
|
"write": "Tulis",
|
||||||
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
||||||
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
|
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
|
||||||
}
|
"askAi": "Tanya AI",
|
||||||
|
"askAiUsageHint": "Digunakan di Terminal SSH",
|
||||||
|
"askAiBaseUrl": "URL dasar",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiApiKey": "Kunci API",
|
||||||
|
"askAiConfigMissing": "Harap konfigurasikan {fields} di Pengaturan.",
|
||||||
|
"askAiConfirmExecute": "Konfirmasi sebelum menjalankan",
|
||||||
|
"askAiCommandInserted": "Perintah dimasukkan ke terminal",
|
||||||
|
"askAiAwaitingResponse": "Menunggu respons AI...",
|
||||||
|
"askAiNoResponse": "Tidak ada respons",
|
||||||
|
"askAiRecommendedCommand": "Perintah yang disarankan AI",
|
||||||
|
"askAiInsertTerminal": "Masukkan ke terminal",
|
||||||
|
"askAiSelectedContent": "Konten yang dipilih",
|
||||||
|
"askAiConversation": "Percakapan AI",
|
||||||
|
"askAiFollowUpHint": "Ajukan pertanyaan lanjutan...",
|
||||||
|
"askAiSend": "Kirim"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
||||||
"write": "書き込み",
|
"write": "書き込み",
|
||||||
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
||||||
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
|
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
|
||||||
}
|
"askAi": "AI に質問",
|
||||||
|
"askAiUsageHint": "SSH ターミナルで使用",
|
||||||
|
"askAiBaseUrl": "ベース URL",
|
||||||
|
"askAiModel": "モデル",
|
||||||
|
"askAiApiKey": "API キー",
|
||||||
|
"askAiConfigMissing": "設定で {fields} を構成してください。",
|
||||||
|
"askAiConfirmExecute": "実行前に確認",
|
||||||
|
"askAiCommandInserted": "コマンドをターミナルに挿入しました",
|
||||||
|
"askAiAwaitingResponse": "AI の応答を待機中...",
|
||||||
|
"askAiNoResponse": "応答なし",
|
||||||
|
"askAiRecommendedCommand": "AI 推奨コマンド",
|
||||||
|
"askAiInsertTerminal": "ターミナルに挿入",
|
||||||
|
"askAiSelectedContent": "選択した内容",
|
||||||
|
"askAiConversation": "AI 会話",
|
||||||
|
"askAiFollowUpHint": "追質問をする...",
|
||||||
|
"askAiSend": "送信"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
||||||
"write": "Schrijven",
|
"write": "Schrijven",
|
||||||
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
||||||
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
|
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
|
||||||
}
|
"askAi": "AI vragen",
|
||||||
|
"askAiUsageHint": "Gebruikt in de SSH-terminal",
|
||||||
|
"askAiBaseUrl": "Basis-URL",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiApiKey": "API-sleutel",
|
||||||
|
"askAiConfigMissing": "Configureer {fields} in de instellingen.",
|
||||||
|
"askAiConfirmExecute": "Bevestigen voor uitvoeren",
|
||||||
|
"askAiCommandInserted": "Commando in terminal ingevoegd",
|
||||||
|
"askAiAwaitingResponse": "Wachten op AI-reactie...",
|
||||||
|
"askAiNoResponse": "Geen reactie",
|
||||||
|
"askAiRecommendedCommand": "Door AI voorgestelde opdracht",
|
||||||
|
"askAiInsertTerminal": "In terminal invoegen",
|
||||||
|
"askAiSelectedContent": "Geselecteerde inhoud",
|
||||||
|
"askAiConversation": "AI-gesprek",
|
||||||
|
"askAiFollowUpHint": "Stel een vervolgvraag...",
|
||||||
|
"askAiSend": "Verzenden"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
||||||
"write": "Escrita",
|
"write": "Escrita",
|
||||||
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
||||||
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script."
|
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
|
||||||
}
|
"askAi": "Perguntar à IA",
|
||||||
|
"askAiUsageHint": "Usado no terminal SSH",
|
||||||
|
"askAiBaseUrl": "URL base",
|
||||||
|
"askAiModel": "Modelo",
|
||||||
|
"askAiApiKey": "Chave de API",
|
||||||
|
"askAiConfigMissing": "Configure {fields} nas configurações.",
|
||||||
|
"askAiConfirmExecute": "Confirmar antes de executar",
|
||||||
|
"askAiCommandInserted": "Comando inserido no terminal",
|
||||||
|
"askAiAwaitingResponse": "Aguardando resposta da IA...",
|
||||||
|
"askAiNoResponse": "Sem resposta",
|
||||||
|
"askAiRecommendedCommand": "Comando sugerido pela IA",
|
||||||
|
"askAiInsertTerminal": "Inserir no terminal",
|
||||||
|
"askAiSelectedContent": "Conteúdo selecionado",
|
||||||
|
"askAiConversation": "Conversa com a IA",
|
||||||
|
"askAiFollowUpHint": "Faça uma pergunta adicional...",
|
||||||
|
"askAiSend": "Enviar"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
||||||
"write": "Запись",
|
"write": "Запись",
|
||||||
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
||||||
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
|
||||||
}
|
"askAi": "Спросить ИИ",
|
||||||
|
"askAiUsageHint": "Используется в SSH-терминале",
|
||||||
|
"askAiBaseUrl": "Базовый URL",
|
||||||
|
"askAiModel": "Модель",
|
||||||
|
"askAiApiKey": "Ключ API",
|
||||||
|
"askAiConfigMissing": "Настройте {fields} в настройках.",
|
||||||
|
"askAiConfirmExecute": "Подтвердите перед выполнением",
|
||||||
|
"askAiCommandInserted": "Команда вставлена в терминал",
|
||||||
|
"askAiAwaitingResponse": "Ожидание ответа ИИ...",
|
||||||
|
"askAiNoResponse": "Нет ответа",
|
||||||
|
"askAiRecommendedCommand": "Команда, предложенная ИИ",
|
||||||
|
"askAiInsertTerminal": "Вставить в терминал",
|
||||||
|
"askAiSelectedContent": "Выбранное содержимое",
|
||||||
|
"askAiConversation": "Разговор с ИИ",
|
||||||
|
"askAiFollowUpHint": "Задайте дополнительный вопрос...",
|
||||||
|
"askAiSend": "Отправить"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
||||||
"write": "Yaz",
|
"write": "Yaz",
|
||||||
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
||||||
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
|
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
|
||||||
}
|
"askAi": "Yapay zekaya sor",
|
||||||
|
"askAiUsageHint": "SSH Terminalinde kullanılır",
|
||||||
|
"askAiBaseUrl": "Temel URL",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiApiKey": "API anahtarı",
|
||||||
|
"askAiConfigMissing": "Lütfen Ayarlar'da {fields} öğesini yapılandırın.",
|
||||||
|
"askAiConfirmExecute": "Çalıştırmadan önce onayla",
|
||||||
|
"askAiCommandInserted": "Komut terminale eklendi",
|
||||||
|
"askAiAwaitingResponse": "Yapay zekâ yanıtı bekleniyor...",
|
||||||
|
"askAiNoResponse": "Yanıt yok",
|
||||||
|
"askAiRecommendedCommand": "YZ önerilen komut",
|
||||||
|
"askAiInsertTerminal": "Terminale ekle",
|
||||||
|
"askAiSelectedContent": "Seçilen içerik",
|
||||||
|
"askAiConversation": "YZ sohbeti",
|
||||||
|
"askAiFollowUpHint": "Yeni bir soru sor...",
|
||||||
|
"askAiSend": "Gönder"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
||||||
"write": "Записати",
|
"write": "Записати",
|
||||||
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
||||||
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
|
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
|
||||||
}
|
"askAi": "Запитати ШІ",
|
||||||
|
"askAiUsageHint": "Використовується в SSH-терміналі",
|
||||||
|
"askAiBaseUrl": "Базова URL",
|
||||||
|
"askAiModel": "Модель",
|
||||||
|
"askAiApiKey": "Ключ API",
|
||||||
|
"askAiConfigMissing": "Налаштуйте {fields} у налаштуваннях.",
|
||||||
|
"askAiConfirmExecute": "Підтвердити перед виконанням",
|
||||||
|
"askAiCommandInserted": "Команду вставлено в термінал",
|
||||||
|
"askAiAwaitingResponse": "Очікування відповіді ШІ...",
|
||||||
|
"askAiNoResponse": "Відповідь відсутня",
|
||||||
|
"askAiRecommendedCommand": "Команда, запропонована ШІ",
|
||||||
|
"askAiInsertTerminal": "Вставити в термінал",
|
||||||
|
"askAiSelectedContent": "Вибраний вміст",
|
||||||
|
"askAiConversation": "Розмова з ШІ",
|
||||||
|
"askAiFollowUpHint": "Поставте додаткове запитання...",
|
||||||
|
"askAiSend": "Надіслати"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
||||||
"write": "写",
|
"write": "写",
|
||||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。",
|
||||||
}
|
"askAi": "问 AI",
|
||||||
|
"askAiUsageHint": "用于 SSH 终端",
|
||||||
|
"askAiBaseUrl": "基础 URL",
|
||||||
|
"askAiModel": "模型",
|
||||||
|
"askAiApiKey": "API 密钥",
|
||||||
|
"askAiConfigMissing": "请前往设置配置 {fields}",
|
||||||
|
"askAiConfirmExecute": "执行前确认",
|
||||||
|
"askAiCommandInserted": "命令已插入终端",
|
||||||
|
"askAiAwaitingResponse": "等待 AI 响应...",
|
||||||
|
"askAiNoResponse": "无回复内容",
|
||||||
|
"askAiRecommendedCommand": "AI 推荐命令",
|
||||||
|
"askAiInsertTerminal": "插入终端",
|
||||||
|
"askAiSelectedContent": "选中的内容",
|
||||||
|
"askAiConversation": "AI 对话",
|
||||||
|
"askAiFollowUpHint": "继续提问...",
|
||||||
|
"askAiSend": "发送"
|
||||||
|
}
|
||||||
|
|||||||
@@ -284,5 +284,21 @@
|
|||||||
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
||||||
"write": "寫入",
|
"write": "寫入",
|
||||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
|
||||||
}
|
"askAi": "詢問 AI",
|
||||||
|
"askAiUsageHint": "於 SSH 終端機中使用",
|
||||||
|
"askAiBaseUrl": "基礎 URL",
|
||||||
|
"askAiModel": "模型",
|
||||||
|
"askAiApiKey": "API 金鑰",
|
||||||
|
"askAiConfigMissing": "請前往設定配置 {fields}",
|
||||||
|
"askAiConfirmExecute": "執行前確認",
|
||||||
|
"askAiCommandInserted": "指令已插入終端機",
|
||||||
|
"askAiAwaitingResponse": "等待 AI 回應...",
|
||||||
|
"askAiNoResponse": "無回覆內容",
|
||||||
|
"askAiRecommendedCommand": "AI 推薦指令",
|
||||||
|
"askAiInsertTerminal": "插入終端機",
|
||||||
|
"askAiSelectedContent": "選取的內容",
|
||||||
|
"askAiConversation": "AI 對話",
|
||||||
|
"askAiFollowUpHint": "繼續提問...",
|
||||||
|
"askAiSend": "傳送"
|
||||||
|
}
|
||||||
|
|||||||
95
lib/view/page/setting/entries/ai.dart
Normal file
95
lib/view/page/setting/entries/ai.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
part of '../entry.dart';
|
||||||
|
|
||||||
|
extension _AI on _AppSettingsPageState {
|
||||||
|
Widget _buildAskAiConfig() {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(LineAwesome.robot_solid, size: _kIconSize),
|
||||||
|
title: TipText(l10n.askAi, l10n.askAiUsageHint),
|
||||||
|
children: [
|
||||||
|
_setting.askAiBaseUrl.listenable().listenVal((val) {
|
||||||
|
final display = val.isEmpty ? libL10n.empty : val;
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(MingCute.link_2_line),
|
||||||
|
title: Text(l10n.askAiBaseUrl),
|
||||||
|
subtitle: Text(display, style: UIs.textGrey, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||||
|
onTap: () => _showAskAiFieldDialog(
|
||||||
|
prop: _setting.askAiBaseUrl,
|
||||||
|
title: l10n.askAiBaseUrl,
|
||||||
|
hint: 'https://api.openai.com',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_setting.askAiModel.listenable().listenVal((val) {
|
||||||
|
final display = val.isEmpty ? libL10n.empty : val;
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.view_module),
|
||||||
|
title: Text(l10n.askAiModel),
|
||||||
|
subtitle: Text(display, style: UIs.textGrey),
|
||||||
|
onTap: () => _showAskAiFieldDialog(
|
||||||
|
prop: _setting.askAiModel,
|
||||||
|
title: l10n.askAiModel,
|
||||||
|
hint: 'gpt-4o-mini',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_setting.askAiApiKey.listenable().listenVal((val) {
|
||||||
|
final hasKey = val.isNotEmpty;
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(MingCute.key_2_line),
|
||||||
|
title: Text(l10n.askAiApiKey),
|
||||||
|
subtitle: Text(hasKey ? '••••••••' : libL10n.empty, style: UIs.textGrey),
|
||||||
|
onTap: () => _showAskAiFieldDialog(
|
||||||
|
prop: _setting.askAiApiKey,
|
||||||
|
title: l10n.askAiApiKey,
|
||||||
|
hint: 'sk-...',
|
||||||
|
obscure: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> _showAskAiFieldDialog({
|
||||||
|
required HiveProp<String> prop,
|
||||||
|
required String title,
|
||||||
|
required String hint,
|
||||||
|
bool obscure = false,
|
||||||
|
}) async {
|
||||||
|
return withTextFieldController((ctrl) async {
|
||||||
|
final fetched = prop.fetch();
|
||||||
|
if (fetched != null && fetched.isNotEmpty) ctrl.text = fetched;
|
||||||
|
|
||||||
|
void onSave() {
|
||||||
|
prop.put(ctrl.text.trim());
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.showRoundDialog(
|
||||||
|
title: title,
|
||||||
|
child: Input(
|
||||||
|
controller: ctrl,
|
||||||
|
autoFocus: true,
|
||||||
|
label: title,
|
||||||
|
hint: hint,
|
||||||
|
icon: obscure ? MingCute.key_2_line : Icons.edit,
|
||||||
|
obscureText: obscure,
|
||||||
|
suggestion: !obscure,
|
||||||
|
onSubmitted: (_) => onSave(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
prop.delete();
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: Text(libL10n.clear),
|
||||||
|
),
|
||||||
|
TextButton(onPressed: onSave, child: Text(libL10n.ok)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,37 +92,37 @@ extension _App on _AppSettingsPageState {
|
|||||||
trailing: _setting.colorSeed.listenable().listenVal((_) {
|
trailing: _setting.colorSeed.listenable().listenVal((_) {
|
||||||
return ClipOval(child: Container(color: UIs.primaryColor, height: 27, width: 27));
|
return ClipOval(child: Container(color: UIs.primaryColor, height: 27, width: 27));
|
||||||
}),
|
}),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final ctrl = TextEditingController(text: UIs.primaryColor.toHex);
|
withTextFieldController((ctrl) async {
|
||||||
await context.showRoundDialog(
|
await context.showRoundDialog(
|
||||||
title: libL10n.primaryColorSeed,
|
title: libL10n.primaryColorSeed,
|
||||||
child: StatefulBuilder(
|
child: StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
/// Plugin [dynamic_color] is not supported on iOS
|
/// Plugin [dynamic_color] is not supported on iOS
|
||||||
if (!isIOS)
|
if (!isIOS)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(l10n.followSystem),
|
title: Text(l10n.followSystem),
|
||||||
trailing: StoreSwitch(
|
trailing: StoreSwitch(
|
||||||
prop: _setting.useSystemPrimaryColor,
|
prop: _setting.useSystemPrimaryColor,
|
||||||
callback: (_) => setState(() {}),
|
callback: (_) => setState(() {}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
];
|
||||||
];
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
if (!_setting.useSystemPrimaryColor.fetch()) {
|
children.add(
|
||||||
children.add(
|
ColorPicker(
|
||||||
ColorPicker(
|
color: Color(_setting.colorSeed.fetch()),
|
||||||
color: Color(_setting.colorSeed.fetch()),
|
onColorChanged: (c) => ctrl.text = c.toHex,
|
||||||
onColorChanged: (c) => ctrl.text = c.toHex,
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
||||||
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
},
|
||||||
},
|
),
|
||||||
),
|
actions: Btn.ok(onTap: () => _onSaveColor(ctrl.text)).toList,
|
||||||
actions: Btn.ok(onTap: () => _onSaveColor(ctrl.text)).toList,
|
);
|
||||||
);
|
});
|
||||||
ctrl.dispose();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,28 +44,28 @@ extension _SFTP on _AppSettingsPageState {
|
|||||||
leading: const Icon(MingCute.edit_fill),
|
leading: const Icon(MingCute.edit_fill),
|
||||||
title: TipText(libL10n.editor, l10n.sftpEditorTip),
|
title: TipText(libL10n.editor, l10n.sftpEditorTip),
|
||||||
trailing: Text(val.isEmpty ? l10n.inner : val, style: UIs.text15),
|
trailing: Text(val.isEmpty ? l10n.inner : val, style: UIs.text15),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final ctrl = TextEditingController(text: val);
|
withTextFieldController((ctrl) async {
|
||||||
void onSave() {
|
void onSave() {
|
||||||
final s = ctrl.text.trim();
|
final s = ctrl.text.trim();
|
||||||
_setting.sftpEditor.put(s);
|
_setting.sftpEditor.put(s);
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.showRoundDialog<bool>(
|
await context.showRoundDialog<bool>(
|
||||||
title: libL10n.select,
|
title: libL10n.select,
|
||||||
child: Input(
|
child: Input(
|
||||||
controller: ctrl,
|
controller: ctrl,
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
label: libL10n.editor,
|
label: libL10n.editor,
|
||||||
hint: '\$EDITOR / vim / nano ...',
|
hint: '\$EDITOR / vim / nano ...',
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
suggestion: false,
|
suggestion: false,
|
||||||
onSubmitted: (_) => onSave(),
|
onSubmitted: (_) => onSave(),
|
||||||
),
|
),
|
||||||
actions: Btn.ok(onTap: onSave).toList,
|
actions: Btn.ok(onTap: onSave).toList,
|
||||||
);
|
);
|
||||||
ctrl.dispose();
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,27 +116,28 @@ extension _SSH on _AppSettingsPageState {
|
|||||||
leading: const Icon(Icons.terminal),
|
leading: const Icon(Icons.terminal),
|
||||||
title: TipText(l10n.terminal, l10n.desktopTerminalTip),
|
title: TipText(l10n.terminal, l10n.desktopTerminalTip),
|
||||||
trailing: Text(val, style: UIs.text15, maxLines: 1, overflow: TextOverflow.ellipsis),
|
trailing: Text(val, style: UIs.text15, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final ctrl = TextEditingController(text: val);
|
withTextFieldController((ctrl) async {
|
||||||
void onSave() {
|
ctrl.text = val;
|
||||||
_setting.desktopTerminal.put(ctrl.text.trim());
|
void onSave() {
|
||||||
context.pop();
|
_setting.desktopTerminal.put(ctrl.text.trim());
|
||||||
}
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
await context.showRoundDialog<bool>(
|
await context.showRoundDialog<bool>(
|
||||||
title: libL10n.select,
|
title: libL10n.select,
|
||||||
child: Input(
|
child: Input(
|
||||||
controller: ctrl,
|
controller: ctrl,
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
label: l10n.terminal,
|
label: l10n.terminal,
|
||||||
hint: 'x-terminal-emulator / gnome-terminal',
|
hint: 'x-terminal-emulator / gnome-terminal',
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
suggestion: false,
|
suggestion: false,
|
||||||
onSubmitted: (_) => onSave(),
|
onSubmitted: (_) => onSave(),
|
||||||
),
|
),
|
||||||
actions: Btn.ok(onTap: onSave).toList,
|
actions: Btn.ok(onTap: onSave).toList,
|
||||||
);
|
);
|
||||||
ctrl.dispose();
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ part 'entries/full_screen.dart';
|
|||||||
part 'entries/server.dart';
|
part 'entries/server.dart';
|
||||||
part 'entries/sftp.dart';
|
part 'entries/sftp.dart';
|
||||||
part 'entries/ssh.dart';
|
part 'entries/ssh.dart';
|
||||||
|
part 'entries/ai.dart';
|
||||||
|
|
||||||
const _kIconSize = 23.0;
|
const _kIconSize = 23.0;
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ final class _AppSettingsPageState extends ConsumerState<AppSettingsPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiList(
|
return MultiList(
|
||||||
children: [
|
children: [
|
||||||
[const CenterGreyTitle('App'), _buildApp()],
|
[const CenterGreyTitle('App'), _buildApp(), const CenterGreyTitle('AI'), _buildAskAiConfig()],
|
||||||
[CenterGreyTitle(l10n.server), _buildServer()],
|
[CenterGreyTitle(l10n.server), _buildServer()],
|
||||||
[const CenterGreyTitle('SSH'), _buildSSH(), const CenterGreyTitle('SFTP'), _buildSFTP()],
|
[const CenterGreyTitle('SSH'), _buildSSH(), const CenterGreyTitle('SFTP'), _buildSFTP()],
|
||||||
[CenterGreyTitle(l10n.container), _buildContainer(), CenterGreyTitle(libL10n.editor), _buildEditor()],
|
[CenterGreyTitle(l10n.container), _buildContainer(), CenterGreyTitle(libL10n.editor), _buildEditor()],
|
||||||
|
|||||||
450
lib/view/page/ssh/page/ask_ai.dart
Normal file
450
lib/view/page/ssh/page/ask_ai.dart
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
part of 'page.dart';
|
||||||
|
|
||||||
|
extension _AskAi on SSHPageState {
|
||||||
|
List<ContextMenuButtonItem> _buildTerminalToolbar(
|
||||||
|
BuildContext context,
|
||||||
|
CustomTextEditState state,
|
||||||
|
List<ContextMenuButtonItem> defaultItems,
|
||||||
|
) {
|
||||||
|
final rawSelection = _termKey.currentState?.renderTerminal.selectedText;
|
||||||
|
final selection = rawSelection?.trim();
|
||||||
|
if (selection == null || selection.isEmpty) {
|
||||||
|
return defaultItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = List<ContextMenuButtonItem>.from(defaultItems);
|
||||||
|
items.add(
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: context.l10n.askAi,
|
||||||
|
onPressed: () {
|
||||||
|
state.hideToolbar();
|
||||||
|
_showAskAiSheet(selection);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showAskAiSheet(String selection) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
final localeHint = Localizations.maybeLocaleOf(context)?.toLanguageTag();
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (ctx) {
|
||||||
|
return _AskAiSheet(selection: selection, localeHint: localeHint, onCommandApply: _applyAiCommand);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyAiCommand(String command) {
|
||||||
|
if (command.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_terminal.textInput(command);
|
||||||
|
(widget.args.focusNode?.requestFocus ?? _termKey.currentState?.requestKeyboard)?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AskAiSheet extends ConsumerStatefulWidget {
|
||||||
|
const _AskAiSheet({required this.selection, required this.localeHint, required this.onCommandApply});
|
||||||
|
|
||||||
|
final String selection;
|
||||||
|
final String? localeHint;
|
||||||
|
final ValueChanged<String> onCommandApply;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AskAiSheet> createState() => _AskAiSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ChatEntryType { user, assistant, command }
|
||||||
|
|
||||||
|
class _ChatEntry {
|
||||||
|
const _ChatEntry._({required this.type, this.content, this.command});
|
||||||
|
|
||||||
|
const _ChatEntry.user(String content) : this._(type: _ChatEntryType.user, content: content);
|
||||||
|
|
||||||
|
const _ChatEntry.assistant(String content) : this._(type: _ChatEntryType.assistant, content: content);
|
||||||
|
|
||||||
|
const _ChatEntry.command(AskAiCommand command) : this._(type: _ChatEntryType.command, command: command);
|
||||||
|
|
||||||
|
final _ChatEntryType type;
|
||||||
|
final String? content;
|
||||||
|
final AskAiCommand? command;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
|
||||||
|
StreamSubscription<AskAiEvent>? _subscription;
|
||||||
|
final _chatEntries = <_ChatEntry>[];
|
||||||
|
final _history = <AskAiMessage>[];
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
final _inputController = TextEditingController();
|
||||||
|
final _seenCommands = <String>{};
|
||||||
|
String? _streamingContent;
|
||||||
|
String? _error;
|
||||||
|
bool _isStreaming = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_inputController.addListener(_handleInputChanged);
|
||||||
|
_startStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_scrollController.dispose();
|
||||||
|
_inputController
|
||||||
|
..removeListener(_handleInputChanged)
|
||||||
|
..dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleInputChanged() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startStream() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
setState(() {
|
||||||
|
_isStreaming = true;
|
||||||
|
_error = null;
|
||||||
|
_streamingContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
final messages = List<AskAiMessage>.from(_history);
|
||||||
|
|
||||||
|
_subscription = ref
|
||||||
|
.read(askAiRepositoryProvider)
|
||||||
|
.ask(selection: widget.selection, localeHint: widget.localeHint, conversation: messages)
|
||||||
|
.listen(
|
||||||
|
_handleEvent,
|
||||||
|
onError: (error, stack) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_error = _describeError(error);
|
||||||
|
_isStreaming = false;
|
||||||
|
_streamingContent = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isStreaming = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEvent(AskAiEvent event) {
|
||||||
|
if (!mounted) return;
|
||||||
|
var shouldScroll = false;
|
||||||
|
setState(() {
|
||||||
|
if (event is AskAiContentDelta) {
|
||||||
|
_streamingContent = (_streamingContent ?? '') + event.delta;
|
||||||
|
shouldScroll = true;
|
||||||
|
} else if (event is AskAiToolSuggestion) {
|
||||||
|
final inserted = _seenCommands.add(event.command.command);
|
||||||
|
if (inserted) {
|
||||||
|
_chatEntries.add(_ChatEntry.command(event.command));
|
||||||
|
shouldScroll = true;
|
||||||
|
}
|
||||||
|
} else if (event is AskAiCompleted) {
|
||||||
|
final fullText = event.fullText.isNotEmpty ? event.fullText : (_streamingContent ?? '');
|
||||||
|
if (fullText.trim().isNotEmpty) {
|
||||||
|
final message = AskAiMessage(role: AskAiMessageRole.assistant, content: fullText);
|
||||||
|
_history.add(message);
|
||||||
|
_chatEntries.add(_ChatEntry.assistant(fullText));
|
||||||
|
}
|
||||||
|
for (final command in event.commands) {
|
||||||
|
final inserted = _seenCommands.add(command.command);
|
||||||
|
if (inserted) {
|
||||||
|
_chatEntries.add(_ChatEntry.command(command));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_streamingContent = null;
|
||||||
|
_isStreaming = false;
|
||||||
|
shouldScroll = true;
|
||||||
|
} else if (event is AskAiStreamError) {
|
||||||
|
_error = _describeError(event.error);
|
||||||
|
_streamingContent = null;
|
||||||
|
_isStreaming = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldScroll) {
|
||||||
|
_scheduleAutoScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleAutoScroll() {
|
||||||
|
if (!_scrollController.hasClients) return;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!_scrollController.hasClients) return;
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _describeError(Object error) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
if (error is AskAiConfigException) {
|
||||||
|
if (error.missingFields.isEmpty) {
|
||||||
|
if (error.hasInvalidBaseUrl) {
|
||||||
|
return 'Invalid Ask AI base URL: ${error.invalidBaseUrl}';
|
||||||
|
}
|
||||||
|
return error.toString();
|
||||||
|
}
|
||||||
|
final locale = Localizations.maybeLocaleOf(context);
|
||||||
|
final separator = switch (locale?.languageCode) {
|
||||||
|
'zh' => '、',
|
||||||
|
'ja' => '、',
|
||||||
|
_ => ', ',
|
||||||
|
};
|
||||||
|
final formattedFields = error.missingFields
|
||||||
|
.map(
|
||||||
|
(field) => switch (field) {
|
||||||
|
AskAiConfigField.baseUrl => l10n.askAiBaseUrl,
|
||||||
|
AskAiConfigField.apiKey => l10n.askAiApiKey,
|
||||||
|
AskAiConfigField.model => l10n.askAiModel,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.join(separator);
|
||||||
|
final message = l10n.askAiConfigMissing(formattedFields);
|
||||||
|
if (error.hasInvalidBaseUrl) {
|
||||||
|
return '$message (invalid URL: ${error.invalidBaseUrl})';
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
if (error is AskAiNetworkException) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return error.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleApplyCommand(BuildContext context, AskAiCommand command) async {
|
||||||
|
final confirmed = await context.showRoundDialog<bool>(
|
||||||
|
title: context.l10n.askAiConfirmExecute,
|
||||||
|
child: SelectableText(command.command, style: const TextStyle(fontFamily: 'monospace')),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||||
|
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.ok)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
widget.onCommandApply(command.command);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackBar(context.l10n.askAiCommandInserted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyCommand(BuildContext context, AskAiCommand command) async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: command.command));
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackBar(libL10n.success);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendMessage() {
|
||||||
|
if (_isStreaming) return;
|
||||||
|
final text = _inputController.text.trim();
|
||||||
|
if (text.isEmpty) return;
|
||||||
|
setState(() {
|
||||||
|
final message = AskAiMessage(role: AskAiMessageRole.user, content: text);
|
||||||
|
_history.add(message);
|
||||||
|
_chatEntries.add(_ChatEntry.user(text));
|
||||||
|
_inputController.clear();
|
||||||
|
});
|
||||||
|
_startStream();
|
||||||
|
_scheduleAutoScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildConversationWidgets(BuildContext context, ThemeData theme) {
|
||||||
|
final widgets = <Widget>[];
|
||||||
|
for (final entry in _chatEntries) {
|
||||||
|
widgets.add(_buildChatItem(context, theme, entry));
|
||||||
|
widgets.add(const SizedBox(height: 12));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_streamingContent != null) {
|
||||||
|
widgets.add(_buildAssistantBubble(theme, content: _streamingContent!, streaming: true));
|
||||||
|
widgets.add(const SizedBox(height: 12));
|
||||||
|
} else if (_chatEntries.isEmpty && _error == null) {
|
||||||
|
widgets.add(_buildAssistantBubble(theme, content: '', streaming: true));
|
||||||
|
widgets.add(const SizedBox(height: 12));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgets.isNotEmpty) {
|
||||||
|
widgets.removeLast();
|
||||||
|
}
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildChatItem(BuildContext context, ThemeData theme, _ChatEntry entry) {
|
||||||
|
switch (entry.type) {
|
||||||
|
case _ChatEntryType.user:
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: CardX(
|
||||||
|
child: Padding(padding: const EdgeInsets.all(12), child: SelectableText(entry.content ?? '')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case _ChatEntryType.assistant:
|
||||||
|
return _buildAssistantBubble(theme, content: entry.content ?? '');
|
||||||
|
case _ChatEntryType.command:
|
||||||
|
final command = entry.command!;
|
||||||
|
return _buildCommandBubble(context, theme, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAssistantBubble(ThemeData theme, {required String content, bool streaming = false}) {
|
||||||
|
final trimmed = content.trim();
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final child = trimmed.isEmpty
|
||||||
|
? Text(
|
||||||
|
streaming ? l10n.askAiAwaitingResponse : l10n.askAiNoResponse,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
)
|
||||||
|
: SimpleMarkdown(data: content);
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CardX(
|
||||||
|
child: Padding(padding: const EdgeInsets.all(12), child: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommandBubble(BuildContext context, ThemeData theme, AskAiCommand command) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CardX(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.askAiRecommendedCommand, style: theme.textTheme.labelMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SelectableText(command.command, style: const TextStyle(fontFamily: 'monospace')),
|
||||||
|
if (command.description.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(command.description, style: theme.textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => _copyCommand(context, command),
|
||||||
|
icon: const Icon(Icons.copy, size: 18),
|
||||||
|
label: Text(libL10n.copy),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => _handleApplyCommand(context, command),
|
||||||
|
icon: const Icon(Icons.terminal, size: 18),
|
||||||
|
label: Text(l10n.askAiInsertTerminal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
|
|
||||||
|
return FractionallySizedBox(
|
||||||
|
heightFactor: 0.85,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.askAi, style: theme.textTheme.titleLarge),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (_isStreaming)
|
||||||
|
const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.askAiSelectedContent, style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
CardX(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: SelectableText(
|
||||||
|
widget.selection,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(context.l10n.askAiConversation, style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
..._buildConversationWidgets(context, theme),
|
||||||
|
if (_error != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
CardX(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (_isStreaming) ...[const SizedBox(height: 16), const LinearProgressIndicator()],
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomPadding),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Input(
|
||||||
|
controller: _inputController,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 4,
|
||||||
|
hint: context.l10n.askAiFollowUpHint,
|
||||||
|
action: TextInputAction.send,
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Btn.icon(
|
||||||
|
onTap: _isStreaming || _inputController.text.trim().isEmpty ? null : _sendMessage,
|
||||||
|
icon: const Icon(Icons.send, size: 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).cardx,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,11 @@ import 'package:server_box/core/chan.dart';
|
|||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/utils/server.dart';
|
import 'package:server_box/core/utils/server.dart';
|
||||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||||
|
import 'package:server_box/data/model/ai/ask_ai_models.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||||
|
import 'package:server_box/data/provider/ai/ask_ai.dart';
|
||||||
import 'package:server_box/data/provider/server/single.dart';
|
import 'package:server_box/data/provider/server/single.dart';
|
||||||
import 'package:server_box/data/provider/snippet.dart';
|
import 'package:server_box/data/provider/snippet.dart';
|
||||||
import 'package:server_box/data/provider/virtual_keyboard.dart';
|
import 'package:server_box/data/provider/virtual_keyboard.dart';
|
||||||
@@ -23,11 +25,11 @@ import 'package:server_box/data/res/store.dart';
|
|||||||
import 'package:server_box/data/res/terminal.dart';
|
import 'package:server_box/data/res/terminal.dart';
|
||||||
import 'package:server_box/data/ssh/session_manager.dart';
|
import 'package:server_box/data/ssh/session_manager.dart';
|
||||||
import 'package:server_box/view/page/storage/sftp.dart';
|
import 'package:server_box/view/page/storage/sftp.dart';
|
||||||
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:xterm/core.dart';
|
import 'package:xterm/core.dart';
|
||||||
import 'package:xterm/ui.dart' hide TerminalThemes;
|
import 'package:xterm/ui.dart' hide TerminalThemes;
|
||||||
|
|
||||||
|
part 'ask_ai.dart';
|
||||||
part 'init.dart';
|
part 'init.dart';
|
||||||
part 'keyboard.dart';
|
part 'keyboard.dart';
|
||||||
part 'virt_key.dart';
|
part 'virt_key.dart';
|
||||||
@@ -247,6 +249,7 @@ class SSHPageState extends ConsumerState<SSHPage>
|
|||||||
viewOffset: Offset(2 * _horizonPadding, CustomAppBar.sysStatusBarHeight),
|
viewOffset: Offset(2 * _horizonPadding, CustomAppBar.sysStatusBarHeight),
|
||||||
hideScrollBar: false,
|
hideScrollBar: false,
|
||||||
focusNode: widget.args.focusNode,
|
focusNode: widget.args.focusNode,
|
||||||
|
toolbarBuilder: _buildTerminalToolbar,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -505,8 +505,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.355"
|
ref: "v1.0.358"
|
||||||
resolved-ref: "73d5f2603859a9f70459d798ed2d267b1d9a86e5"
|
resolved-ref: c8e55d054875bb3ccdab9894a01fe82d173dc54e
|
||||||
url: "https://github.com/lppcg/fl_lib"
|
url: "https://github.com/lppcg/fl_lib"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -1862,8 +1862,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v4.0.4"
|
ref: "v4.0.11"
|
||||||
resolved-ref: "5747837cdb7b113ef733ce0104e4f2bfa1eb4a36"
|
resolved-ref: "74546b4c4e81357448445c2b6f92411e72dbefec"
|
||||||
url: "https://github.com/lollipopkit/xterm.dart"
|
url: "https://github.com/lollipopkit/xterm.dart"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ dependencies:
|
|||||||
xterm:
|
xterm:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/xterm.dart
|
url: https://github.com/lollipopkit/xterm.dart
|
||||||
ref: v4.0.4
|
ref: v4.0.11
|
||||||
computer:
|
computer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/dart_computer
|
url: https://github.com/lollipopkit/dart_computer
|
||||||
@@ -65,7 +65,7 @@ dependencies:
|
|||||||
fl_lib:
|
fl_lib:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_lib
|
url: https://github.com/lppcg/fl_lib
|
||||||
ref: v1.0.355
|
ref: v1.0.358
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# webdav_client_plus:
|
# webdav_client_plus:
|
||||||
|
|||||||
Reference in New Issue
Block a user